<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Dennison Bertram</title>
    <description>The latest articles on DEV Community by Dennison Bertram (@dennisonbertram).</description>
    <link>https://dev.to/dennisonbertram</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3572813%2Facf865c6-793b-455a-91db-cfc769f1cc36.png</url>
      <title>DEV Community: Dennison Bertram</title>
      <link>https://dev.to/dennisonbertram</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/dennisonbertram"/>
    <language>en</language>
    <item>
      <title>I Let My AI Agent Create Its Own Databases. Here's How the Passwords Stay Locked Up.</title>
      <dc:creator>Dennison Bertram</dc:creator>
      <pubDate>Thu, 29 Oct 2026 14:29:55 +0000</pubDate>
      <link>https://dev.to/dennisonbertram/i-let-my-ai-agent-create-its-own-databases-heres-how-the-passwords-stay-locked-up-b8o</link>
      <guid>https://dev.to/dennisonbertram/i-let-my-ai-agent-create-its-own-databases-heres-how-the-passwords-stay-locked-up-b8o</guid>
      <description>&lt;p&gt;There's a moment in almost every project when the app needs to remember something. My agent has written the code and shipped it, and now it needs a database. For a long time, that was where I took over. I'd click around a dashboard, copy a password, paste it into a &lt;code&gt;.env&lt;/code&gt; file, and then paste it into the chat so the agent could use it.&lt;/p&gt;

&lt;p&gt;By the end of that, the password lived in four places, and I'd chosen none of them on purpose. So I started wondering what it would look like if the agent could just ask for a database, wire it up, and never show the password to anyone, including me.&lt;/p&gt;

&lt;p&gt;This post walks through how &lt;a href="https://github.com/dennisonbertram/agentic-hosting" rel="noopener noreferrer"&gt;agentic-hosting&lt;/a&gt; does that. It's an open-source tool I built that turns a Linux server into a place where AI agents can deploy apps and databases on their own. By the end you'll know how to create a Postgres or Redis database with one API call, connect it to an app, keep its password out of chat logs, and remove it cleanly.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;⭐ If this is useful, a star on &lt;a href="https://github.com/dennisonbertram/agentic-hosting" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; helps other people find it.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;🤖 &lt;strong&gt;Give this to your agent:&lt;/strong&gt; point it at &lt;a href="https://raw.githubusercontent.com/dennisonbertram/agentic-hosting/main/llms.txt" rel="noopener noreferrer"&gt;https://raw.githubusercontent.com/dennisonbertram/agentic-hosting/main/llms.txt&lt;/a&gt; and it can sign up and set things up from that one link.&lt;/p&gt;

&lt;h2&gt;
  
  
  A few words first
&lt;/h2&gt;

&lt;p&gt;agentic-hosting is one program, &lt;code&gt;ah&lt;/code&gt;, that runs on your server. Agents talk to it over an HTTP API. Each agent (or team) is a tenant with its own API key, and an app it runs is called a service.&lt;/p&gt;

&lt;p&gt;The examples below use two shell variables: &lt;code&gt;AH_URL&lt;/code&gt; is your server's address and &lt;code&gt;AH_KEY&lt;/code&gt; is the tenant's API key. The commands use &lt;code&gt;curl&lt;/code&gt; and &lt;code&gt;jq&lt;/code&gt;, which is also how an agent drives it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Asking for a database
&lt;/h2&gt;

&lt;p&gt;One request creates a database. The type is &lt;code&gt;postgres&lt;/code&gt; or &lt;code&gt;redis&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;IDEM&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;uuidgen&lt;span class="si"&gt;)&lt;/span&gt;   &lt;span class="c"&gt;# reuse this on retries&lt;/span&gt;
curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$AH_URL&lt;/span&gt;&lt;span class="s2"&gt;/v1/databases"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$AH_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Idempotency-Key: &lt;/span&gt;&lt;span class="nv"&gt;$IDEM&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"name":"notes-db","type":"postgres"}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The call doesn't come back until the database actually works. &lt;code&gt;ah&lt;/code&gt; pulls the image (&lt;code&gt;postgres:16-alpine&lt;/code&gt; or &lt;code&gt;redis:7-alpine&lt;/code&gt;), starts a container, and then checks readiness by speaking the database's own protocol, not just by seeing an open port. Once the image is on the box this takes about 30 seconds. On a fresh server the pull can take longer, so the route allows up to six minutes.&lt;/p&gt;

&lt;p&gt;That's why the &lt;code&gt;Idempotency-Key&lt;/code&gt; header matters. Agents retry. If the connection drops halfway and the agent sends the same request with the same key, &lt;code&gt;ah&lt;/code&gt; waits for the first one to finish and hands back its result, instead of making a second database. It remembers a successful result for 10 minutes.&lt;/p&gt;

&lt;p&gt;You get back a &lt;code&gt;201&lt;/code&gt; with something like this (trimmed):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"3f9c0a…"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"notes-db"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"postgres"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ready"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"host"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ah-db-3f9c0a…"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"port"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;5432&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"db_name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ah"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"username"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ah"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"connection_string"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"postgres://ah:…"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Names start with a letter, can use letters, numbers, hyphens, and underscores, and can be up to 63 characters. For Postgres, the database and user are both called &lt;code&gt;ah&lt;/code&gt;. The password is 32 random bytes written as 64 hex characters. Nobody picks it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the password goes
&lt;/h2&gt;

&lt;p&gt;This is the part I care about most. Before anything is saved, &lt;code&gt;ah&lt;/code&gt; encrypts both the password and the full connection string with AES-256-GCM, using the server's master key. Only the encrypted versions go into &lt;code&gt;ah&lt;/code&gt;'s own database.&lt;/p&gt;

&lt;p&gt;The plain connection string shows up in exactly two places:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;the response to the create call&lt;/li&gt;
&lt;li&gt;a dedicated endpoint for fetching it
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$AH_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$AH_URL&lt;/span&gt;&lt;span class="s2"&gt;/v1/databases/&lt;/span&gt;&lt;span class="nv"&gt;$DB_ID&lt;/span&gt;&lt;span class="s2"&gt;/connection-string"&lt;/span&gt;
&lt;span class="c"&gt;# {"connection_string":"postgres://ah:…"}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every call to that endpoint writes an audit line to the server log with the tenant, the database, and the ID of the API key that asked. Listing or reading the database (&lt;code&gt;GET /v1/databases&lt;/code&gt; and &lt;code&gt;GET /v1/databases/{id}&lt;/code&gt;) never includes it.&lt;/p&gt;

&lt;p&gt;Two honest notes. First, there's one master key for the whole server, and right now it encrypts everything directly: env vars, database passwords, connection strings. A design for deriving a separate key for each of those uses is in the repo but not built yet. Tenants are kept apart by the API, which scopes every lookup to the tenant's own records, not by separate keys. Second, "encrypted at rest" here means the credentials &lt;code&gt;ah&lt;/code&gt; stores. The rows inside your Postgres sit in a normal Docker volume, and traffic between your app and the database is plain TCP (&lt;code&gt;sslmode=disable&lt;/code&gt;) on a private network that only your services can join.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wiring it into an app
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;ah&lt;/code&gt; doesn't add &lt;code&gt;DATABASE_URL&lt;/code&gt; to your app on its own. You set it, then restart. Those are two calls, and the connection string goes straight from one to the other without being printed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;CONN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$AH_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$AH_URL&lt;/span&gt;&lt;span class="s2"&gt;/v1/databases/&lt;/span&gt;&lt;span class="nv"&gt;$DB_ID&lt;/span&gt;&lt;span class="s2"&gt;/connection-string"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  | jq &lt;span class="nt"&gt;-r&lt;/span&gt; .connection_string&lt;span class="si"&gt;)&lt;/span&gt;

jq &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="nt"&gt;--arg&lt;/span&gt; u &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$CONN&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s1"&gt;'{DATABASE_URL: $u}'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  | curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$AH_URL&lt;/span&gt;&lt;span class="s2"&gt;/v1/services/&lt;/span&gt;&lt;span class="nv"&gt;$SVC_ID&lt;/span&gt;&lt;span class="s2"&gt;/env"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$AH_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; @-

curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$AH_URL&lt;/span&gt;&lt;span class="s2"&gt;/v1/services/&lt;/span&gt;&lt;span class="nv"&gt;$SVC_ID&lt;/span&gt;&lt;span class="s2"&gt;/restart"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$AH_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Setting env merges new keys into what's there. The response is only &lt;code&gt;{"status":"updated", ...}&lt;/code&gt;, with no values in it. Environment variables are encrypted with the same master key. If you read them back, every value comes back as &lt;code&gt;********&lt;/code&gt; unless you add &lt;code&gt;?reveal=true&lt;/code&gt;, and a reveal is also written to the audit log.&lt;/p&gt;

&lt;p&gt;The restart matters more than it looks. It recreates the container, which is the only way new environment variables take effect, and it's also when the app gets connected to its database. More on that next.&lt;/p&gt;

&lt;p&gt;For Redis it's the same flow. Use &lt;code&gt;REDIS_URL&lt;/code&gt; as the name by convention, and the string looks like &lt;code&gt;redis://:&amp;lt;password&amp;gt;@ah-db-&amp;lt;id&amp;gt;:6379/0&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  How the app finds the database
&lt;/h2&gt;

&lt;p&gt;Look at the host in the connection string: &lt;code&gt;ah-db-&amp;lt;id&amp;gt;&lt;/code&gt;, not an IP address. That's on purpose, and it's something I got wrong at first.&lt;/p&gt;

&lt;p&gt;Early versions handed out &lt;code&gt;127.0.0.1:&amp;lt;port&amp;gt;&lt;/code&gt;. That looks fine until you remember the app runs in its own container, where &lt;code&gt;127.0.0.1&lt;/code&gt; is the container itself. The app was knocking on its own door. It was reported as &lt;a href="https://github.com/dennisonbertram/agentic-hosting/issues/164" rel="noopener noreferrer"&gt;issue #164&lt;/a&gt; and fixed in October.&lt;/p&gt;

&lt;p&gt;Now each database gets its own private Docker network, named &lt;code&gt;ah-dbnet-&amp;lt;tenant&amp;gt;-&amp;lt;db&amp;gt;&lt;/code&gt;, with no route to the internet. On that network the database answers to the name &lt;code&gt;ah-db-&amp;lt;id&amp;gt;&lt;/code&gt; on its normal port, 5432 or 6379. When a service is deployed, started, or restarted, &lt;code&gt;ah&lt;/code&gt; looks at its environment variables. If one of them mentions one of that tenant's databases, &lt;code&gt;ah&lt;/code&gt; connects the service to that database's network.&lt;/p&gt;

&lt;p&gt;Two details follow from this. Only the tenant's own databases are checked, so another tenant can't get in by copying your connection string into its app. And the name only works from inside a service container. Running &lt;code&gt;psql&lt;/code&gt; against it from the server itself won't resolve, and that's expected.&lt;/p&gt;

&lt;p&gt;If you're upgrading from an older version, &lt;code&gt;ah&lt;/code&gt; rewrites the old &lt;code&gt;127.0.0.1&lt;/code&gt; strings on startup, both on the database and in any service env that copied it. Running apps still need one restart to pick up the new value. The fix is covered by tests in the repo, and the design note still lists a check on a live gVisor host as a to-do. If something doesn't connect for you, I'd like to &lt;a href="https://github.com/dennisonbertram/agentic-hosting/issues" rel="noopener noreferrer"&gt;hear about it&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Or let your agent do it
&lt;/h2&gt;

&lt;p&gt;All of the above is one command with &lt;code&gt;ahc&lt;/code&gt;, the project's command-line client:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ahc db provision notes-api postgres
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It creates a database named &lt;code&gt;notes-api-postgres&lt;/code&gt; (pick another with &lt;code&gt;--name&lt;/code&gt;), waits for it, sets &lt;code&gt;DATABASE_URL&lt;/code&gt; (or &lt;code&gt;REDIS_URL&lt;/code&gt; for Redis) on the service, and restarts it. It prints only the host, not the password. If you use Claude Code, the repo also ships an &lt;code&gt;/ah-db&lt;/code&gt; slash command that does the same steps and is told never to print the full connection string.&lt;/p&gt;

&lt;p&gt;In practice I mostly say "this app needs Postgres" and let the agent pick one of those.&lt;/p&gt;

&lt;h2&gt;
  
  
  The limits
&lt;/h2&gt;

&lt;p&gt;This is deliberately one simple path, not a database service with every option:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Limit&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Engines&lt;/td&gt;
&lt;td&gt;Postgres 16, Redis 7&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Databases per tenant&lt;/td&gt;
&lt;td&gt;3 by default&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Memory per database&lt;/td&gt;
&lt;td&gt;512 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sandbox&lt;/td&gt;
&lt;td&gt;gVisor, all Linux capabilities dropped except 5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Disk&lt;/td&gt;
&lt;td&gt;blocked at 90% full&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Going past the quota returns a &lt;code&gt;409&lt;/code&gt;. If the disk is 90% full or more, creates fail with a &lt;code&gt;503&lt;/code&gt; and a &lt;code&gt;Retry-After&lt;/code&gt; header, so the agent knows to wait instead of retrying in a loop. There's no MySQL, no custom images, and no replicas or failover.&lt;/p&gt;

&lt;h2&gt;
  
  
  Backups are a job for your agent
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;ah&lt;/code&gt; keeps your data in a Docker volume named &lt;code&gt;ah-db-&amp;lt;id&amp;gt;&lt;/code&gt;, but it doesn't back that volume up. Its own backup command covers its control-plane state, not your app's rows. This is the part I hand to my agent, because it's a well-defined chore:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Every night, dump each Postgres database
with pg_dump inside its container, gzip it,
copy it to my backup host, and keep 7 days.
Restore last night's dump into a scratch
database once a week to prove it works.
Alert me if a dump is missing or empty.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The agent writes the timer, the script, and the restore check, and I review them once. The big platforms used to sell redundancy, backups, and monitoring as features. An agent can build the same pieces on your own server, and you can read exactly what it built.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cleaning up
&lt;/h2&gt;

&lt;p&gt;Delete in the opposite order you created. Point the app away from the database first, so it isn't left with a broken URL:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-X&lt;/span&gt; DELETE &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$AH_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$AH_URL&lt;/span&gt;&lt;span class="s2"&gt;/v1/services/&lt;/span&gt;&lt;span class="nv"&gt;$SVC_ID&lt;/span&gt;&lt;span class="s2"&gt;/env/DATABASE_URL"&lt;/span&gt;
curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$AH_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$AH_URL&lt;/span&gt;&lt;span class="s2"&gt;/v1/services/&lt;/span&gt;&lt;span class="nv"&gt;$SVC_ID&lt;/span&gt;&lt;span class="s2"&gt;/restart"&lt;/span&gt;

curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-X&lt;/span&gt; DELETE &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$AH_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$AH_URL&lt;/span&gt;&lt;span class="s2"&gt;/v1/databases/&lt;/span&gt;&lt;span class="nv"&gt;$DB_ID&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The delete returns &lt;code&gt;204&lt;/code&gt;. &lt;code&gt;ah&lt;/code&gt; stops and removes the container, then overwrites every file in the volume with zeros, so a later tenant on the same disk can't recover the data. Then it removes the volume, the record, and the private network. With the CLI it's &lt;code&gt;ahc db delete notes-db --confirm&lt;/code&gt;, and the &lt;code&gt;--confirm&lt;/code&gt; is required.&lt;/p&gt;

&lt;p&gt;If something is left behind anyway, a background cleanup loop removes &lt;code&gt;ah-db-*&lt;/code&gt; volumes that no database record owns, and database networks nothing owns once they're 15 minutes old.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;An agent on agentic-hosting gets a working Postgres or Redis database with one call. The password is random, encrypted before it's stored, and only handed out through one audited endpoint, and the app reaches the database over a private network that's set up when it restarts. Pair it with a nightly dump your agent writes, and delete it cleanly when you're done.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/dennisonbertram/agentic-hosting" rel="noopener noreferrer"&gt;Star agentic-hosting on GitHub&lt;/a&gt; so more people find it.&lt;/li&gt;
&lt;li&gt;Give it to your agent: &lt;a href="https://raw.githubusercontent.com/dennisonbertram/agentic-hosting/main/llms.txt" rel="noopener noreferrer"&gt;llms.txt&lt;/a&gt; to sign up and set up, or the &lt;a href="https://raw.githubusercontent.com/dennisonbertram/agentic-hosting/main/skills/agentic-hosting/SKILL.md" rel="noopener noreferrer"&gt;agent skill&lt;/a&gt; for daily use.&lt;/li&gt;
&lt;li&gt;Try the &lt;a href="https://dennisonbertram.github.io/agentic-hosting/docs/install/" rel="noopener noreferrer"&gt;quickstart&lt;/a&gt;, or read the &lt;a href="https://dennisonbertram.github.io/agentic-hosting/docs/databases/" rel="noopener noreferrer"&gt;Postgres &amp;amp; Redis page&lt;/a&gt; on the &lt;a href="https://dennisonbertram.github.io/agentic-hosting/" rel="noopener noreferrer"&gt;docs site&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/dennisonbertram/agentic-hosting/issues" rel="noopener noreferrer"&gt;Open an issue&lt;/a&gt; if you'd want built-in database backups or separate encryption keys next. I'd like to know which one you'd use first.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;Cover photo: storage tanks of the National Molasses Company on the Houston Ship Channel, with refineries behind. Blair Pittman, June 1973, for the EPA's Documerica project. U.S. National Archives, public domain.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>postgres</category>
      <category>selfhosted</category>
      <category>devops</category>
      <category>ai</category>
    </item>
    <item>
      <title>My Hosting Platform Runs on Two SQLite Files. Here's How I Back Them Up and Bring Them Back.</title>
      <dc:creator>Dennison Bertram</dc:creator>
      <pubDate>Tue, 27 Oct 2026 14:17:48 +0000</pubDate>
      <link>https://dev.to/dennisonbertram/my-hosting-platform-runs-on-two-sqlite-files-heres-how-i-back-them-up-and-bring-them-back-51h</link>
      <guid>https://dev.to/dennisonbertram/my-hosting-platform-runs-on-two-sqlite-files-heres-how-i-back-them-up-and-bring-them-back-51h</guid>
      <description>&lt;p&gt;When people hear that the thing running my server keeps all its state in SQLite, they usually ask the same question. Isn't that a toy database? Shouldn't a hosting platform sit on Postgres, with a replica and a team that knows how to fail it over?&lt;/p&gt;

&lt;p&gt;I went back and forth on this for a while. Where I ended up is that the database itself was never the scary part. The scary part is a question nobody likes to answer about their own setup. If this machine disappeared tonight, how long would it take to get everything back? With a couple of small files, the honest answer can be "a few minutes", and you can prove it by doing it.&lt;/p&gt;

&lt;p&gt;This post walks through how &lt;a href="https://github.com/dennisonbertram/agentic-hosting" rel="noopener noreferrer"&gt;agentic-hosting&lt;/a&gt; stores its state. It's an open-source tool I built that turns a Linux server into a place where AI agents can deploy apps and databases on their own. By the end you'll know where its data lives, how to take a safe backup while it's running, what else you need to copy, and how to restore everything on a fresh server.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;⭐ If this is useful, a star on &lt;a href="https://github.com/dennisonbertram/agentic-hosting" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; helps other people find it.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;🤖 &lt;strong&gt;Give this to your agent:&lt;/strong&gt; point it at &lt;a href="https://raw.githubusercontent.com/dennisonbertram/agentic-hosting/main/llms.txt" rel="noopener noreferrer"&gt;https://raw.githubusercontent.com/dennisonbertram/agentic-hosting/main/llms.txt&lt;/a&gt; and it can sign up and set things up from that one link.&lt;/p&gt;

&lt;h2&gt;
  
  
  A few words first
&lt;/h2&gt;

&lt;p&gt;agentic-hosting is one program, &lt;code&gt;ah&lt;/code&gt;, that runs on your server. Agents talk to it over an HTTP API to deploy apps, add Postgres or Redis, and read logs. &lt;code&gt;ah&lt;/code&gt; is the control plane. It doesn't run your apps itself. It keeps track of them and tells Docker what to do.&lt;/p&gt;

&lt;p&gt;That record keeping is what this post is about. Every account, API key, app, build, environment variable, and database &lt;code&gt;ah&lt;/code&gt; knows about is a row in SQLite, a database that lives in a single file on disk. There's no database server to install or keep running.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the data lives
&lt;/h2&gt;

&lt;p&gt;On a standard install, everything sits in &lt;code&gt;/var/lib/ah&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/var/lib/ah/
├── ah.db            # state: tenants, keys,
│                    # services, builds, env
├── ah-metering.db   # usage samples
├── master.key       # encryption key
└── backups/         # output of `ah backup`
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There are two database files, not one, and I'll come back to why. You'll also see &lt;code&gt;ah.db-wal&lt;/code&gt; and &lt;code&gt;ah.db-shm&lt;/code&gt; next to them while &lt;code&gt;ah&lt;/code&gt; is running. Those belong to SQLite, and they matter a lot for backups.&lt;/p&gt;

&lt;p&gt;You can look inside with the regular &lt;code&gt;sqlite3&lt;/code&gt; tool. This lists the schema changes the database has been through:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;sqlite3 /var/lib/ah/ah.db &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s1"&gt;'SELECT name FROM schema_migrations;'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;ah&lt;/code&gt; ships its schema as numbered SQL files built into the binary. On startup it applies any it hasn't recorded yet, each one in its own transaction along with the row that records it. Right now that's 26 files for the state database and 2 for metering. When you upgrade, the new binary applies its new files the first time it starts.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why SQLite holds up here
&lt;/h2&gt;

&lt;p&gt;Three settings do most of the work. &lt;code&gt;ah&lt;/code&gt; opens both files like this (wrapped to fit):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;sql&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"sqlite3"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;
  &lt;span class="s"&gt;"?_journal_mode=WAL"&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;
  &lt;span class="s"&gt;"&amp;amp;_busy_timeout=5000"&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;
  &lt;span class="s"&gt;"&amp;amp;_foreign_keys=on"&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;
  &lt;span class="s"&gt;"&amp;amp;_txlock=immediate"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;WAL mode.&lt;/strong&gt; By default SQLite writes changes straight into the main file, and readers wait while that happens. In WAL (write-ahead log) mode, new writes go to the &lt;code&gt;-wal&lt;/code&gt; file first and get folded into the main file later. Readers keep reading while a write is in progress. For a control plane, where an agent polling build status shouldn't stall another agent's deploy, this is the setting that matters most.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A busy timeout.&lt;/strong&gt; SQLite still allows only one writer at a time. With &lt;code&gt;_busy_timeout=5000&lt;/code&gt;, a second writer waits up to 5 seconds for its turn instead of failing at once.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Take the write lock up front.&lt;/strong&gt; This one took me a while to understand. A normal SQLite transaction starts as a reader and asks for the write lock only when it first writes. If another connection committed in between, SQLite returns an error right away and the busy timeout never kicks in. &lt;code&gt;_txlock=immediate&lt;/code&gt; makes every transaction ask for the write lock at the start, so it waits its turn like everyone else. The repo has a test for exactly this case.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;ah&lt;/code&gt; also caps each file at 4 open connections, which is plenty for one server. And one server is the real limit of this design. Everything assumes one machine, and the docs list multi-node as a known gap. I'm fine with that. One machine with good backups is a setup I can understand at 2am.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why two files
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;ah.db&lt;/code&gt; holds the things you'd be upset to lose: accounts, keys, apps, builds, encrypted environment variables, and database passwords. &lt;code&gt;ah-metering.db&lt;/code&gt; holds usage. Every 60 seconds &lt;code&gt;ah&lt;/code&gt; records a CPU and memory sample for each running app, in two tables, &lt;code&gt;usage_events&lt;/code&gt; and &lt;code&gt;usage_daily&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The way I think about it, those are different kinds of data. Metering writes on a timer, forever, and it grows with every app you run. State changes when something actually happens. Since SQLite allows one writer per file, putting the steady stream of samples in its own file means it never queues behind, or in front of, a deploy. The split also shows up in backups. If the metering backup fails, &lt;code&gt;ah backup&lt;/code&gt; logs a warning and carries on. If the state backup fails, the whole run fails.&lt;/p&gt;

&lt;p&gt;The code keeps the two apart on purpose. Each migration file has to start with &lt;code&gt;state_&lt;/code&gt; or &lt;code&gt;metering_&lt;/code&gt;, and &lt;code&gt;ah&lt;/code&gt; refuses to start if one doesn't, so a schema change can't end up in the wrong database.&lt;/p&gt;

&lt;h2&gt;
  
  
  Taking a backup while it's running
&lt;/h2&gt;

&lt;p&gt;Here's the trap with WAL mode. Recent writes can sit in &lt;code&gt;ah.db-wal&lt;/code&gt; for a while before they reach &lt;code&gt;ah.db&lt;/code&gt;. If you &lt;code&gt;cp ah.db&lt;/code&gt; somewhere, you can get a file that's missing recent changes, or one that's inconsistent.&lt;/p&gt;

&lt;p&gt;SQLite has a command for this, &lt;code&gt;VACUUM INTO&lt;/code&gt;. It reads the database through SQLite itself, WAL included, and writes a clean standalone copy to a new file. It works on a read-only connection, so the backup never takes the write lock away from the running server.&lt;/p&gt;

&lt;p&gt;That's what &lt;code&gt;ah backup&lt;/code&gt; does:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; ah /usr/local/bin/ah backup &lt;span class="se"&gt;\&lt;/span&gt;
  /var/lib/ah/ah.db
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Step by step, it:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;creates &lt;code&gt;backups/&lt;/code&gt; next to the database, readable only by its owner&lt;/li&gt;
&lt;li&gt;stops if that disk is 90% full or more, and warns at 80%&lt;/li&gt;
&lt;li&gt;opens &lt;code&gt;ah.db&lt;/code&gt; read-only and runs &lt;code&gt;VACUUM INTO&lt;/code&gt; a temp file, with a 5 minute timeout&lt;/li&gt;
&lt;li&gt;gzips the copy to &lt;code&gt;ah-YYYYMMDD-HHMMSS.db.gz&lt;/code&gt;, writing to a &lt;code&gt;.tmp&lt;/code&gt; file and renaming it at the end, so a half-written backup never has a real name&lt;/li&gt;
&lt;li&gt;does the same for &lt;code&gt;ah-metering.db&lt;/code&gt; if it exists&lt;/li&gt;
&lt;li&gt;keeps the 10 newest state backups and the 10 newest metering backups, and deletes older ones&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Afterwards you'll see something like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/var/lib/ah/backups/
├── ah-20261027-091700.db.gz
└── ah-metering-20261027-091700.db.gz
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A quick sanity check that a backup is a real SQLite file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;sh &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s1"&gt;'gunzip -c /var/lib/ah/backups/ah-2*.db.gz'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; 15
&lt;span class="c"&gt;# SQLite format 3&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To run it every hour, a cron entry is enough:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# /etc/cron.d/ah-backup
17 * * * * ah /usr/local/bin/ah backup /var/lib/ah/ah.db
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What the backup doesn't include
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;.db.gz&lt;/code&gt; files aren't everything you need:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Item&lt;/th&gt;
&lt;th&gt;Why you need it&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;master.key&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;decrypts env vars and DB passwords&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/etc/default/ah&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;bootstrap token and settings&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Docker volumes&lt;/td&gt;
&lt;td&gt;your Postgres and Redis data&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;local registry&lt;/td&gt;
&lt;td&gt;images you built on the box&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The master key is the big one. Secrets in &lt;code&gt;ah.db&lt;/code&gt; are encrypted with it, and &lt;code&gt;ah backup&lt;/code&gt; never writes it into a backup. Store it somewhere other than your database backups, because anyone holding both can read your secrets.&lt;/p&gt;

&lt;p&gt;One more detail. If you rotate the key with &lt;code&gt;ah rotate-key&lt;/code&gt;, older backups still need the old key. Keep each key for as long as you keep backups made with it.&lt;/p&gt;

&lt;p&gt;And your apps' actual data, the rows in the Postgres databases &lt;code&gt;ah&lt;/code&gt; created for you, lives in Docker volumes named &lt;code&gt;ah-db-*&lt;/code&gt;. That needs its own backup. The SQLite file only knows those databases exist.&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting copies off the box
&lt;/h2&gt;

&lt;p&gt;A backup that lives on the machine it's protecting doesn't protect much. This is the kind of chore I hand to my agent. A prompt like this works:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Every hour, after ah backup runs, rsync
/var/lib/ah/backups/ to my backup host.
Copy master.key to my secrets manager once,
and again after any key rotation. Alert me
if the newest backup is more than 2 hours
old.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The agent writes the timer, the copy script, and the check. I review them once. The big platforms used to give you redundancy, backups, and monitoring as a feature. Now an agent can build the same pieces on your own box in an afternoon.&lt;/p&gt;

&lt;p&gt;If an hour of possible loss is too much, the repo has an optional &lt;a href="https://litestream.io/" rel="noopener noreferrer"&gt;Litestream&lt;/a&gt; setup (&lt;code&gt;deploy/litestream.yml&lt;/code&gt; and a runbook in &lt;code&gt;docs/ops/&lt;/code&gt;) that streams both databases to S3-compatible storage as they change. &lt;code&gt;ah&lt;/code&gt; doesn't install or manage it. You run it as its own service.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bringing it back
&lt;/h2&gt;

&lt;p&gt;There's no &lt;code&gt;ah restore&lt;/code&gt; command. A restore is a few file operations, which I like, because there's nothing extra that has to work on the worst day. On the new server, install &lt;code&gt;ah&lt;/code&gt; the normal way and copy over &lt;code&gt;/etc/default/ah&lt;/code&gt;. Put the backup files in &lt;code&gt;/var/lib/ah/backups/&lt;/code&gt; and the key in your home folder. Then, as root:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;systemctl stop ah
&lt;span class="nb"&gt;cd&lt;/span&gt; /var/lib/ah
&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; pre-restore
&lt;span class="nb"&gt;mv &lt;/span&gt;ah.db&lt;span class="k"&gt;*&lt;/span&gt; ah-metering.db&lt;span class="k"&gt;*&lt;/span&gt; pre-restore/

&lt;span class="nv"&gt;TS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;20261027-091700
&lt;span class="nb"&gt;gunzip&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; backups/ah-&lt;span class="nv"&gt;$TS&lt;/span&gt;.db.gz &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; ah.db
&lt;span class="nb"&gt;gunzip&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; backups/ah-metering-&lt;span class="nv"&gt;$TS&lt;/span&gt;.db.gz &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; ah-metering.db
&lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-m&lt;/span&gt; 600 &lt;span class="nt"&gt;-o&lt;/span&gt; ah &lt;span class="nt"&gt;-g&lt;/span&gt; ah &lt;span class="se"&gt;\&lt;/span&gt;
  ~/master.key master.key

&lt;span class="nb"&gt;chown &lt;/span&gt;ah:ah ah.db ah-metering.db
&lt;span class="nb"&gt;chmod &lt;/span&gt;600 ah.db ah-metering.db
systemctl start ah
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Moving the old &lt;code&gt;-wal&lt;/code&gt; and &lt;code&gt;-shm&lt;/code&gt; files out of the way is the step people skip. If a stale WAL file sits next to the restored database, SQLite can try to replay it into the restored file. If you skip the metering file, &lt;code&gt;ah&lt;/code&gt; starts with an empty one and you lose usage history, nothing else.&lt;/p&gt;

&lt;p&gt;Then check it's up. The API wants HTTPS, so a local request has to say it came through the proxy:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&gt;'X-Forwarded-Proto: https'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  http://127.0.0.1:8080/v1/system/health
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you restored with the wrong &lt;code&gt;master.key&lt;/code&gt;, you'll know right away. On startup &lt;code&gt;ah&lt;/code&gt; decrypts a key it uses to check API keys, and if that fails it refuses to start, with &lt;code&gt;wrong master key?&lt;/code&gt; in the error in &lt;code&gt;journalctl -u ah&lt;/code&gt;. I like that it fails loudly. A server that started and then quietly rejected every API key would be much worse.&lt;/p&gt;

&lt;h2&gt;
  
  
  What happens to the apps
&lt;/h2&gt;

&lt;p&gt;The restored database describes apps that may not be running on the new machine. &lt;code&gt;ah&lt;/code&gt; doesn't trust those rows. Every 30 seconds a loop called the reconciler compares the database with what Docker actually has. An app marked &lt;code&gt;running&lt;/code&gt; with no container gets marked &lt;code&gt;crashed&lt;/code&gt;, with the reason &lt;code&gt;container not found by reconciler&lt;/code&gt;. Nothing restarts on its own.&lt;/p&gt;

&lt;p&gt;To bring an app back, redeploy it with &lt;code&gt;ahc&lt;/code&gt;, the project's command-line client:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ahc service redeploy my-api
ahc status
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A redeploy starts the app again from the image it already had. If that image was built on the old server and you didn't copy the local registry, it won't be there. Start a fresh build from git instead, the same way the app was deployed the first time.&lt;/p&gt;

&lt;p&gt;Or tell your agent "the server was restored, redeploy everything that was running." It can list the services, see which ones are marked &lt;code&gt;crashed&lt;/code&gt;, and work through them one at a time.&lt;/p&gt;

&lt;p&gt;Then do the test that actually matters. Open an app that uses a secret, like a database password, and make sure it works. That's the proof that the key and the database belong together.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;agentic-hosting keeps its whole control plane in two SQLite files: &lt;code&gt;ah.db&lt;/code&gt; for state and &lt;code&gt;ah-metering.db&lt;/code&gt; for usage. WAL mode keeps reads and writes out of each other's way, &lt;code&gt;ah backup&lt;/code&gt; uses &lt;code&gt;VACUUM INTO&lt;/code&gt; to take consistent copies while the server runs, and a restore is just putting the files and the master key back. Have your agent ship the copies off the box, and run the restore once on a scratch server before you need it.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/dennisonbertram/agentic-hosting" rel="noopener noreferrer"&gt;Star agentic-hosting on GitHub&lt;/a&gt; so more people find it.&lt;/li&gt;
&lt;li&gt;Give it to your agent: &lt;a href="https://raw.githubusercontent.com/dennisonbertram/agentic-hosting/main/llms.txt" rel="noopener noreferrer"&gt;llms.txt&lt;/a&gt; to sign up and set up, or the &lt;a href="https://raw.githubusercontent.com/dennisonbertram/agentic-hosting/main/skills/agentic-hosting/SKILL.md" rel="noopener noreferrer"&gt;agent skill&lt;/a&gt; for daily use.&lt;/li&gt;
&lt;li&gt;Try the &lt;a href="https://dennisonbertram.github.io/agentic-hosting/docs/install/" rel="noopener noreferrer"&gt;quickstart&lt;/a&gt;, or read the &lt;a href="https://dennisonbertram.github.io/agentic-hosting/docs/backups/" rel="noopener noreferrer"&gt;backups page&lt;/a&gt; on the &lt;a href="https://dennisonbertram.github.io/agentic-hosting/" rel="noopener noreferrer"&gt;docs site&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/dennisonbertram/agentic-hosting/issues" rel="noopener noreferrer"&gt;Open an issue&lt;/a&gt; if you'd want a built-in &lt;code&gt;ah restore&lt;/code&gt; or volume backups for managed Postgres. I'd like to know which one you'd use first.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;Cover photo: seats and fittings from old Amtrak passenger cars piled in a warehouse at Mira Loma, California, waiting to be renovated or replaced. Charles O'Rear, May 1974, for the EPA's Documerica project. U.S. National Archives, public domain.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>sqlite</category>
      <category>devops</category>
      <category>selfhosted</category>
      <category>ai</category>
    </item>
    <item>
      <title>When your coding model refuses your own AppSec work</title>
      <dc:creator>Dennison Bertram</dc:creator>
      <pubDate>Mon, 26 Oct 2026 17:33:10 +0000</pubDate>
      <link>https://dev.to/dennisonbertram/when-your-coding-model-refuses-your-own-appsec-work-139c</link>
      <guid>https://dev.to/dennisonbertram/when-your-coding-model-refuses-your-own-appsec-work-139c</guid>
      <description>&lt;p&gt;If you do application security, you have probably watched a coding model decline something routine: writing a proof of concept for a bug in your own staging app, or explaining how a flaw in your own handler would be exploited so you can fix it. That leaves the defender with less help than an attacker would have.&lt;/p&gt;

&lt;p&gt;I work on free-pi, a free terminal coding agent. We are adding an opt-in &lt;strong&gt;Defense&lt;/strong&gt; section to the &lt;code&gt;/model&lt;/code&gt; picker for signed-in GitHub users. The first model is &lt;strong&gt;Defense Flash&lt;/strong&gt;: a reasoning model with tool use and a 1M-token context, chosen for lawful security work that general models often refuse. It is billed from normal credits like any other model. We have not published tests of it, so this post is about using it responsibly, not a benchmark.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ground rules
&lt;/h2&gt;

&lt;p&gt;Use it for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;systems and code you own,&lt;/li&gt;
&lt;li&gt;employer or client systems where you have written permission to test,&lt;/li&gt;
&lt;li&gt;in-scope bug bounty targets, following the program's rules,&lt;/li&gt;
&lt;li&gt;software you own or may lawfully analyze (free-pi's &lt;code&gt;/reverse-engineer-anything&lt;/code&gt; command covers native binaries, JS and Electron apps, APKs, .NET and websites).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Do not use it on anyone else's systems. Computer misuse laws apply whichever model you pick, and a bounty program's scope list is a boundary.&lt;/p&gt;

&lt;h2&gt;
  
  
  Habits that help
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;State the context: who owns the system and what you are authorized to do.&lt;/li&gt;
&lt;li&gt;Aim at find-and-fix: ask for the minimal patch and a regression test.&lt;/li&gt;
&lt;li&gt;Test in isolation: a local container or disposable staging copy, never production, no real credentials. free-pi has no OS sandbox for normal sessions.&lt;/li&gt;
&lt;li&gt;Verify claims: reproduce a reported flaw before you file it, and review every generated patch.&lt;/li&gt;
&lt;li&gt;Keep the scope in the session: for bounty work, paste the program's scope text.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Pairing
&lt;/h2&gt;

&lt;p&gt;free-pi bundles the audit method and checklists from the &lt;a href="https://github.com/cloudflare/security-audit-skill" rel="noopener noreferrer"&gt;security-audit skill&lt;/a&gt;, run in a single agent, so validate each finding yourself. &lt;code&gt;/reverse-engineer-anything&lt;/code&gt; covers software you may lawfully analyze. The Defense section is where to look when a general model declines the lawful part.&lt;/p&gt;

&lt;p&gt;More at &lt;a href="https://freepi.ai/blog/using-defense-mode-responsibly" rel="noopener noreferrer"&gt;freepi.ai&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>security</category>
      <category>ai</category>
      <category>devtools</category>
      <category>productivity</category>
    </item>
    <item>
      <title>A practical CI and supply-chain hardening checklist you can run with a terminal agent</title>
      <dc:creator>Dennison Bertram</dc:creator>
      <pubDate>Mon, 26 Oct 2026 17:32:14 +0000</pubDate>
      <link>https://dev.to/dennisonbertram/a-practical-ci-and-supply-chain-hardening-checklist-you-can-run-with-a-terminal-agent-11h5</link>
      <guid>https://dev.to/dennisonbertram/a-practical-ci-and-supply-chain-hardening-checklist-you-can-run-with-a-terminal-agent-11h5</guid>
      <description>&lt;p&gt;Your CI is code that runs with your secrets and publishes what your users install. Cloudflare's open-source &lt;a href="https://github.com/cloudflare/security-audit-skill" rel="noopener noreferrer"&gt;security-audit skill&lt;/a&gt; includes a &lt;code&gt;SUPPLY-CHAIN-AND-RELEASE.md&lt;/code&gt; reference whose core rules are useful questions to ask your own pipeline, even without running a full review:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A known-vulnerable or mutable dependency is not a finding by itself. Ask who can influence resolution and what the build does next.&lt;/li&gt;
&lt;li&gt;Follow integrity across every handoff, from source to the artifact a user runs.&lt;/li&gt;
&lt;li&gt;CI configuration is authorization code: which event triggered it, whose code runs, which secrets exist, what can it publish?&lt;/li&gt;
&lt;li&gt;A checksum fetched from the same untrusted place as the artifact proves nothing independent.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I work on free-pi, a free terminal agent that bundles the skill's audit method and reference checklists. It runs them in a single agent, without sub-agents or an OS sandbox, so check every claim the agent makes yourself. This guide is illustrative, not a recorded run. Only review pipelines you own or have permission to review.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ask for a read-only review first
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;review my CI and release setup for supply-chain risks, using the
supply-chain reference in the security-audit skill. Do not change files yet.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Seven common GitHub Actions checks
&lt;/h2&gt;

&lt;p&gt;These are general good practice, not a list from the skill. You can verify each one yourself.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Unpinned actions: pin &lt;code&gt;uses:&lt;/code&gt; to a full commit SHA.&lt;/li&gt;
&lt;li&gt;Broad token permissions: set a top-level &lt;code&gt;permissions:&lt;/code&gt; with the least needed.&lt;/li&gt;
&lt;li&gt;Untrusted input in &lt;code&gt;run:&lt;/code&gt; steps: PR titles and branch names can inject shell commands; pass them via env vars and quote.&lt;/li&gt;
&lt;li&gt;Risky triggers: &lt;code&gt;pull_request_target&lt;/code&gt; and &lt;code&gt;workflow_run&lt;/code&gt; can run with secrets while touching fork code.&lt;/li&gt;
&lt;li&gt;Secret reach: limit which jobs can read publish tokens; prefer short-lived credentials.&lt;/li&gt;
&lt;li&gt;Provenance: know which commit built each published artifact, and let users verify it.&lt;/li&gt;
&lt;li&gt;Lockfiles and install scripts: install from committed lockfiles and review packages with install scripts.
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;permissions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;contents&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;read&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Fix one thing per PR
&lt;/h2&gt;

&lt;p&gt;Ask the agent for one change at a time and review each diff yourself. CI files guard your secrets, so do not merge what you do not understand, and run the workflow on a branch first.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it cannot see
&lt;/h2&gt;

&lt;p&gt;Branch protection, registry permissions, hosted-runner settings and signing-service configuration live outside the repo. Check them in your hosting and registry settings.&lt;/p&gt;

&lt;p&gt;free-pi has an opt-in Defense model section in &lt;code&gt;/model&lt;/code&gt; for when a general model declines lawful work on your own pipeline. More at &lt;a href="https://freepi.ai/blog/harden-ci-and-supply-chain-with-a-terminal-agent" rel="noopener noreferrer"&gt;freepi.ai&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>security</category>
      <category>devops</category>
      <category>github</category>
      <category>cicd</category>
    </item>
    <item>
      <title>A structured security review of your own repo, from the terminal</title>
      <dc:creator>Dennison Bertram</dc:creator>
      <pubDate>Mon, 26 Oct 2026 17:32:13 +0000</pubDate>
      <link>https://dev.to/dennisonbertram/a-structured-security-review-of-your-own-repo-from-the-terminal-jh4</link>
      <guid>https://dev.to/dennisonbertram/a-structured-security-review-of-your-own-repo-from-the-terminal-jh4</guid>
      <description>&lt;p&gt;Most "AI security review" prompts give you a pile of plausible-sounding warnings. Cloudflare's open-source &lt;a href="https://github.com/cloudflare/security-audit-skill" rel="noopener noreferrer"&gt;security-audit skill&lt;/a&gt; (MIT) describes a more disciplined method: map the code, hunt by coverage, try to disprove every candidate, and write findings as structured records.&lt;/p&gt;

&lt;p&gt;I work on free-pi, a free terminal coding agent that bundles the skill's audit method and reference checklists. This post explains the method and how to check its output. It is a guide, not a recorded run, and the commands are illustrative. Only review code you own or have permission to test.&lt;/p&gt;

&lt;h2&gt;
  
  
  The six phases in the skill's README
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Reconnaissance: map architecture, trust boundaries and input surfaces into &lt;code&gt;architecture.md&lt;/code&gt; and &lt;code&gt;coverage-ledger.json&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Coverage-led hunting: work through units from the ledger.&lt;/li&gt;
&lt;li&gt;Candidate validation: try to disprove each candidate.&lt;/li&gt;
&lt;li&gt;Structured output: &lt;code&gt;findings.json&lt;/code&gt; with &lt;code&gt;confirmed&lt;/code&gt;, &lt;code&gt;needs_validation&lt;/code&gt; and &lt;code&gt;rejected&lt;/code&gt; records, checked against a JSON schema.&lt;/li&gt;
&lt;li&gt;Record verification: re-check source claims.&lt;/li&gt;
&lt;li&gt;Reporting: &lt;code&gt;REPORT.md&lt;/code&gt;, &lt;code&gt;FINDINGS-DETAIL.md&lt;/code&gt; and &lt;code&gt;NEEDS-VALIDATION.md&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The skill also has reference files of attack classes for web and auth protocols, client-side code, supply chain, cloud deployment, memory safety and more.&lt;/p&gt;

&lt;h2&gt;
  
  
  What free-pi does differently from the skill's design
&lt;/h2&gt;

&lt;p&gt;The skill's README expects an agent that can run parallel sub-agents, so the agent that finds a problem is never the agent that checks it, plus an OS-enforced sandbox for running target code. free-pi has neither. It runs the skill in a single agent that follows the method itself.&lt;/p&gt;

&lt;p&gt;So the independent checking in the skill's design does not happen. The same agent raises and judges each candidate, and you should validate every finding yourself. Do not ask it to build or run code from a repo you do not trust.&lt;/p&gt;

&lt;h2&gt;
  
  
  Starting it
&lt;/h2&gt;

&lt;p&gt;From your repo root:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;security audit this codebase
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A direct request asks for the full method; narrower questions get lighter guidance. The skill's default output directory is &lt;code&gt;~/security-audit-skill/&amp;lt;repo-name&amp;gt;/run-&amp;lt;N&amp;gt;&lt;/code&gt;, outside your repo.&lt;/p&gt;

&lt;h2&gt;
  
  
  Checking the results
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;confirmed&lt;/code&gt;: follow the source trace in the code yourself.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;needs_validation&lt;/code&gt;: each should name one unknown fact, and you often know the answer.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;rejected&lt;/code&gt;: skim to see what was set aside.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The skill's authors say a single run found roughly half of what repeated runs found, so run it again after your first round of fixes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limits
&lt;/h2&gt;

&lt;p&gt;It reads code. It cannot see your cloud settings, registry permissions or branch protection. Treat every finding as a lead to verify, not a verdict.&lt;/p&gt;

&lt;p&gt;To try it: &lt;code&gt;npx free-pi-cli&lt;/code&gt;, sign in with GitHub, and ask for a security audit. More at &lt;a href="https://freepi.ai/blog/audit-your-repo-with-security-audit-skill" rel="noopener noreferrer"&gt;freepi.ai&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>security</category>
      <category>ai</category>
      <category>devops</category>
      <category>opensource</category>
    </item>
    <item>
      <title>I Taught My Coding Agent to Run My Server With One Text File. Here's How to Write One for Yours.</title>
      <dc:creator>Dennison Bertram</dc:creator>
      <pubDate>Sun, 25 Oct 2026 14:14:09 +0000</pubDate>
      <link>https://dev.to/dennisonbertram/i-taught-my-coding-agent-to-run-my-server-with-one-text-file-heres-how-to-write-one-for-yours-1p07</link>
      <guid>https://dev.to/dennisonbertram/i-taught-my-coding-agent-to-run-my-server-with-one-text-file-heres-how-to-write-one-for-yours-1p07</guid>
      <description>&lt;p&gt;For a while I had a ritual. Every time I opened a new chat with my coding agent, I pasted in the same paragraph: here's my server's address, here's how you deploy, the build field is called this and not that, don't do the other thing. If I forgot, the agent did what any smart newcomer does. It guessed. It guessed field names that sounded right and weren't. It called endpoints that should exist and don't.&lt;/p&gt;

&lt;p&gt;The agent wasn't the problem. It had never seen my server. Nobody had given it the manual.&lt;/p&gt;

&lt;p&gt;That paragraph is now a file. It sits in a folder on my laptop, the agent reads it when the conversation turns to my server, and I haven't pasted anything in months. I think this is one of the most underrated tricks in working with agents right now, and it takes about an hour to do for your own setup.&lt;/p&gt;

&lt;p&gt;This post explains how those files work, using &lt;a href="https://github.com/dennisonbertram/agentic-hosting" rel="noopener noreferrer"&gt;agentic-hosting&lt;/a&gt; as the example. It's an open-source tool I built that turns a Linux server into a place where AI agents can deploy apps and databases on their own. By the end you'll know how to install its agent instructions, use its six shortcut commands, and write the same kind of file for your own infrastructure.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;⭐ If this is useful, a star on &lt;a href="https://github.com/dennisonbertram/agentic-hosting" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; helps other people find it.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;🤖 &lt;strong&gt;Give this to your agent:&lt;/strong&gt; point it at &lt;a href="https://raw.githubusercontent.com/dennisonbertram/agentic-hosting/main/llms.txt" rel="noopener noreferrer"&gt;https://raw.githubusercontent.com/dennisonbertram/agentic-hosting/main/llms.txt&lt;/a&gt; and it can sign up and set things up from that one link.&lt;/p&gt;

&lt;h2&gt;
  
  
  A few words first
&lt;/h2&gt;

&lt;p&gt;I use Claude Code, Anthropic's coding agent that runs in your terminal, so the examples here are for it. Two terms:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A &lt;strong&gt;skill&lt;/strong&gt; is a folder with a file called &lt;code&gt;SKILL.md&lt;/code&gt; in it. The file has a short header saying what the skill is for, then plain instructions. Claude reads the instructions when your request matches the header.&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;slash command&lt;/strong&gt; is a shortcut you type, like &lt;code&gt;/ah-deploy&lt;/code&gt;. It's also just a markdown file. When you type the command, Claude Code drops that file's text into the conversation as instructions.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Skills follow an open format called &lt;a href="https://agentskills.io" rel="noopener noreferrer"&gt;Agent Skills&lt;/a&gt;, so other agent tools can read the same files. Slash commands are a Claude Code feature.&lt;/p&gt;

&lt;p&gt;agentic-hosting itself is one program, &lt;code&gt;ah&lt;/code&gt;, running on your server. Agents talk to it over an HTTP API. &lt;code&gt;$AH_URL&lt;/code&gt; below is your server's address and &lt;code&gt;$AH_KEY&lt;/code&gt; is the agent's API key.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a skill file looks like
&lt;/h2&gt;

&lt;p&gt;Here's the top of the skill I ship, trimmed and wrapped:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;agentic-hosting&lt;/span&gt;
&lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;This skill should be used when&lt;/span&gt;
  &lt;span class="s"&gt;the user asks to install, set up, or operate&lt;/span&gt;
  &lt;span class="s"&gt;agentic-hosting... Trigger on "deploy a&lt;/span&gt;
  &lt;span class="s"&gt;service", "provision a database", "register&lt;/span&gt;
  &lt;span class="s"&gt;a tenant", "view logs", ...&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;

&lt;span class="gh"&gt;# agentic-hosting&lt;/span&gt;

Fetch this file from a raw GitHub URL and
follow it end to end. You do not need a GUI.
Every action is curl against &lt;span class="sb"&gt;`/v1/`&lt;/span&gt;.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The part between the &lt;code&gt;---&lt;/code&gt; lines is the header. The part I didn't expect to matter so much is &lt;code&gt;description&lt;/code&gt;. Claude Code keeps every installed skill's description in view all the time, but it only loads the full body when your request looks like a match. So the description isn't a summary for people. It's a list of the things you'd actually say when you want this skill. "Deploy a service." "View logs." If I'd written "a self-hosted PaaS control plane", Claude would rarely connect that to "put my app online."&lt;/p&gt;

&lt;p&gt;That loading rule also means a long skill is cheap until it's used. You can put a lot of detail in the body without paying for it in every conversation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two copies, two jobs
&lt;/h2&gt;

&lt;p&gt;agentic-hosting ships its instructions in two shapes, because people use agents in two ways.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One file you can hand to any agent.&lt;/strong&gt; The standalone skill lives at &lt;a href="https://raw.githubusercontent.com/dennisonbertram/agentic-hosting/main/skills/agentic-hosting/SKILL.md" rel="noopener noreferrer"&gt;&lt;code&gt;skills/agentic-hosting/SKILL.md&lt;/code&gt;&lt;/a&gt;. It's about 300 lines and covers everything: installing the server, creating an account, deploying from git, adding Postgres or Redis, reading logs, and the full API map. Any agent that can read a URL can follow it. &lt;code&gt;llms.txt&lt;/code&gt; (the link in the callout above) is the index that points to it and to the rest of the docs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A Claude Code bundle.&lt;/strong&gt; The repo's &lt;code&gt;.claude/&lt;/code&gt; folder has a fuller skill, four reference files it can open when it needs detail, and six slash commands. One line installs all of it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://agentic.hosting/install-skill.sh | bash
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That puts the skill and its reference files in &lt;code&gt;~/.claude/skills/agentic-hosting/&lt;/code&gt; and the commands in &lt;code&gt;~/.claude/commands/&lt;/code&gt;. Files in your home folder work in every project on that machine. Then tell it where your server is, in your shell profile:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;AH_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;https://your-server
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;AH_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;keyid.secret
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you only want the single file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;R&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;https://raw.githubusercontent.com
&lt;span class="nv"&gt;P&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;dennisonbertram/agentic-hosting/main
&lt;span class="nv"&gt;D&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;~/.claude/skills/agentic-hosting
&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$D&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$D&lt;/span&gt;&lt;span class="s2"&gt;/SKILL.md"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$R&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="nv"&gt;$P&lt;/span&gt;&lt;span class="s2"&gt;/skills/agentic-hosting/SKILL.md"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Using it
&lt;/h2&gt;

&lt;p&gt;The easiest way is to just talk. Once the skill is installed, these all work without any special syntax:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;deploy https://github.com/me/blog as "blog-web"
on port 3000, then show me the build output

add a postgres database to blog-web

what's the status of my services?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Claude matches the request to the skill, reads the instructions, and runs the &lt;code&gt;curl&lt;/code&gt; calls. You can also call the skill by name with &lt;code&gt;/agentic-hosting&lt;/code&gt;, since the folder name doubles as a command.&lt;/p&gt;

&lt;p&gt;The six slash commands are shortcuts for the jobs I do most:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Command&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/ah-deploy&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Deploy from a git URL or Docker image&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/ah-status&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Disk, services, databases, builds&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/ah-db&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Add Postgres or Redis to an app&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/ah-logs&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Stream app logs, or a build's logs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/ah-register&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Create an account, get an API key&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/ah-snapshot&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Snapshot an app before a risky change&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;code&gt;/ah-status&lt;/code&gt; is the one I use daily. It reads disk use, lists every app with its status and crash count, flags anything not running, and checks for builds in progress. It's my morning "is anything on fire" check, and when something is, the same skill tells the agent how to handle it: what a tripped crash breaker looks like, how to reset it and redeploy, what to do when the disk is nearly full. The agent works the incident instead of handing me an error.&lt;/p&gt;

&lt;p&gt;One note on &lt;code&gt;/ah-register&lt;/code&gt;: it reads the server's setup secret over SSH, so it's for whoever runs the server, not for an agent that's just been handed a key.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's inside a slash command
&lt;/h2&gt;

&lt;p&gt;Here's the top of &lt;code&gt;/ah-deploy&lt;/code&gt;, trimmed and wrapped:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Deploy a service to&lt;/span&gt;
  &lt;span class="s"&gt;agentic-hosting from a git URL or Docker&lt;/span&gt;
  &lt;span class="s"&gt;image.&lt;/span&gt;
&lt;span class="na"&gt;argument-hint&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;&amp;lt;git-url-or-image&amp;gt;&lt;/span&gt;
  &lt;span class="s"&gt;&amp;lt;service-name&amp;gt; [port]&lt;/span&gt;
&lt;span class="na"&gt;allowed-tools&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Bash&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;

You are an agentic-hosting operator. Use
&lt;span class="sb"&gt;`$AH_URL`&lt;/span&gt; and &lt;span class="sb"&gt;`$AH_KEY`&lt;/span&gt; from the environment
(ask the user if not set).
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Below that is a numbered list: check the server is up, stop if the disk is over 90% full, create the app, start the build, stream the log, wait up to 10 minutes for it to be running, report the URL.&lt;/p&gt;

&lt;p&gt;Two lines in the header deserve attention.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;argument-hint&lt;/code&gt; is what you see while typing the command. It's documentation for you, nothing more.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;allowed-tools: Bash&lt;/code&gt; lets Claude run shell commands without asking you first, for the turn where you invoked the command. That's why &lt;code&gt;/ah-deploy&lt;/code&gt; runs without a stack of permission prompts. It's also something to decide on purpose. For a deploy command, I want it. For a command that deletes things, I'd leave it off and approve each step. Claude Code also has a &lt;code&gt;disable-model-invocation: true&lt;/code&gt; setting for commands Claude shouldn't start on its own, which is worth using for anything with side effects.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bug I found while writing this
&lt;/h2&gt;

&lt;p&gt;To write this post I checked every command file against the current Claude Code docs, and found a real problem in mine.&lt;/p&gt;

&lt;p&gt;Slash commands can pull in what you typed after the command. My command files were written when &lt;code&gt;$1&lt;/code&gt; meant the first argument. In current Claude Code, numbering starts at zero: &lt;code&gt;$0&lt;/code&gt; is the first argument and &lt;code&gt;$1&lt;/code&gt; is the second. So when you type this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/ah-deploy https://github.com/me/blog blog-web 3000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;the instructions Claude gets say to deploy &lt;code&gt;3000&lt;/code&gt; using &lt;code&gt;blog-web&lt;/code&gt;, and the git URL never appears in the instructions at all. Nothing errors. The agent just starts from a scrambled brief.&lt;/p&gt;

&lt;p&gt;Until the files are fixed, plain English (the examples above) is the reliable way to deploy. If you write your own commands, two habits avoid this. Use &lt;code&gt;$ARGUMENTS&lt;/code&gt;, which is everything you typed, and let Claude sort it out. Or name your arguments in the header (&lt;code&gt;arguments: [source, service, port]&lt;/code&gt;) and write &lt;code&gt;$source&lt;/code&gt; and &lt;code&gt;$service&lt;/code&gt; in the body, so the numbering can't shift under you.&lt;/p&gt;

&lt;p&gt;The bigger lesson is that these files are code that nothing compiles. They drift from your API, and from the agent tool, and nobody gets an error when they do.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to write one for your own infrastructure
&lt;/h2&gt;

&lt;p&gt;This is the part I'd steal if I were you. You don't need a platform like mine. Any internal API, deploy script, or runbook works. Here's what I've learned makes these files good.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Write the description in your own words.&lt;/strong&gt; List the phrases you'd actually type. Claude truncates long descriptions in its listing (about 1,500 characters), so lead with the common ones.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Show the exact request, and name the wrong one.&lt;/strong&gt; The most useful line in my skill is a negative: "There is no &lt;code&gt;git_url&lt;/code&gt; / &lt;code&gt;branch&lt;/code&gt; field." Agents kept inventing those names because they sound right. Writing down the plausible mistake fixed it in a way that just showing the right shape didn't.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Say what done looks like.&lt;/strong&gt; My skill doesn't say "wait for the deploy." It gives a loop that checks every 5 seconds for up to 10 minutes and stops on one of three things: running, failed, or the crash breaker open. Agents are great at following a loop and bad at deciding when to give up.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Keep the main file short and link out.&lt;/strong&gt; Anthropic's docs suggest keeping &lt;code&gt;SKILL.md&lt;/code&gt; under 500 lines. Mine is about 400, and the full API listing, operations notes, domain setup, and server install live in four separate files the skill points to. Claude opens them only when it needs them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Put the habits in writing.&lt;/strong&gt; My database command says never print the full connection string, only host and port. That's an instruction, not a lock. The string still passes through the commands Claude runs. Write the habit down anyway. It works most of the time, and the real protection belongs on the server.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;6. Test it like code, against the real thing.&lt;/strong&gt; Open a fresh session, ask for the task in plain words, and compare what the agent does with what the server accepts. Every time I've done this I've found something. Earlier checks caught my skill reading the build's ID from a field called &lt;code&gt;id&lt;/code&gt; when the server returns &lt;code&gt;build_id&lt;/code&gt;, and waiting for a status string the server never sends. Both are fixed now. This round turned up the argument numbering above, plus a line in both skill files that says "redeploy" rebuilds from git. It doesn't. It restarts the app with the image it already has and the current settings.&lt;/p&gt;

&lt;p&gt;Here's a skeleton to start from. Swap in your own endpoints:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;my-deploys&lt;/span&gt;
&lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Use when the user asks to deploy,&lt;/span&gt;
  &lt;span class="s"&gt;roll back, check deploy status, or read&lt;/span&gt;
  &lt;span class="s"&gt;logs for our apps on deploy.internal.&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;

&lt;span class="gh"&gt;# Deploying our apps&lt;/span&gt;

Base URL: $DEPLOY_URL. Auth header:
&lt;span class="sb"&gt;`Authorization: Bearer $DEPLOY_TOKEN`&lt;/span&gt;.

&lt;span class="gu"&gt;## Deploy&lt;/span&gt;
POST /deploys {"app":"&lt;span class="nt"&gt;&amp;lt;name&amp;gt;&lt;/span&gt;","ref":"&lt;span class="nt"&gt;&amp;lt;sha&amp;gt;&lt;/span&gt;"}
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="sb"&gt;`ref`&lt;/span&gt; must be a full commit SHA. There is
  no &lt;span class="sb"&gt;`branch`&lt;/span&gt; field.
&lt;span class="p"&gt;-&lt;/span&gt; Poll GET /deploys/&lt;span class="nt"&gt;&amp;lt;id&amp;gt;&lt;/span&gt; every 5s, up to
  10 min. Stop on "live" or "failed".

&lt;span class="gu"&gt;## Never&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Print tokens or connection strings.
&lt;span class="p"&gt;-&lt;/span&gt; Deploy to prod without saying so first.

&lt;span class="gu"&gt;## More detail&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; references/api.md for every endpoint.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Save it as &lt;code&gt;~/.claude/skills/my-deploys/SKILL.md&lt;/code&gt;, start a new session, and ask it to deploy something. Then read what it did.&lt;/p&gt;

&lt;h2&gt;
  
  
  The skill isn't the permission
&lt;/h2&gt;

&lt;p&gt;One last thing I'd tell anyone doing this. The skill file teaches the agent how. It shouldn't decide what the agent is allowed to do. That belongs in the API key.&lt;/p&gt;

&lt;p&gt;In agentic-hosting, the key you get when you create an account is an owner key. Extra keys you create for agents are member keys by default. A member key can deploy, add databases, and read logs, but it can't revoke keys or suspend the account. So my agents run with member keys, and the skill can be as capable as I like without any one agent being able to lock me out.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;A skill is the paragraph you keep pasting, saved as a file with a header that tells the agent when to read it. agentic-hosting ships one file any agent can follow, plus a Claude Code bundle with six shortcut commands, and the same recipe works for your own infrastructure: real phrases in the description, exact requests with the wrong ones named, a clear stopping point, and regular testing against the real API.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/dennisonbertram/agentic-hosting" rel="noopener noreferrer"&gt;Star agentic-hosting on GitHub&lt;/a&gt; so more people find it.&lt;/li&gt;
&lt;li&gt;Give it to your agent: &lt;a href="https://raw.githubusercontent.com/dennisonbertram/agentic-hosting/main/llms.txt" rel="noopener noreferrer"&gt;llms.txt&lt;/a&gt; to sign up and set up, or the &lt;a href="https://raw.githubusercontent.com/dennisonbertram/agentic-hosting/main/skills/agentic-hosting/SKILL.md" rel="noopener noreferrer"&gt;agent skill&lt;/a&gt; for daily use.&lt;/li&gt;
&lt;li&gt;Try the &lt;a href="https://dennisonbertram.github.io/agentic-hosting/docs/install/" rel="noopener noreferrer"&gt;quickstart&lt;/a&gt;, or start at the &lt;a href="https://dennisonbertram.github.io/agentic-hosting/" rel="noopener noreferrer"&gt;docs site&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/dennisonbertram/agentic-hosting/issues" rel="noopener noreferrer"&gt;Open an issue&lt;/a&gt; if you've written a skill for your own infra and found a pattern that works. I'd like to collect them.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;Cover photo: members of an outdoor-orientation workshop learning to read maps and use compasses in Warrensburg, New York. Anne LaBastille, October 1973, for the EPA's Documerica project. U.S. National Archives, public domain.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>claude</category>
      <category>devops</category>
      <category>selfhosted</category>
    </item>
    <item>
      <title>My AI Agents Share a Server. Here's How I Keep Them Out of Each Other's Way.</title>
      <dc:creator>Dennison Bertram</dc:creator>
      <pubDate>Fri, 23 Oct 2026 14:13:48 +0000</pubDate>
      <link>https://dev.to/dennisonbertram/my-ai-agents-share-a-server-heres-how-i-keep-them-out-of-each-others-way-enj</link>
      <guid>https://dev.to/dennisonbertram/my-ai-agents-share-a-server-heres-how-i-keep-them-out-of-each-others-way-enj</guid>
      <description>&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;This post shows how to give each AI agent its own account on a server running &lt;a href="https://github.com/dennisonbertram/agentic-hosting" rel="noopener noreferrer"&gt;agentic-hosting&lt;/a&gt;, 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.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;⭐ If this is useful, a star on &lt;a href="https://github.com/dennisonbertram/agentic-hosting" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; helps other people find it.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;🤖 &lt;strong&gt;Give this to your agent:&lt;/strong&gt; point it at &lt;a href="https://raw.githubusercontent.com/dennisonbertram/agentic-hosting/main/llms.txt" rel="noopener noreferrer"&gt;https://raw.githubusercontent.com/dennisonbertram/agentic-hosting/main/llms.txt&lt;/a&gt; and it can sign up and set things up from that one link.&lt;/p&gt;

&lt;h2&gt;
  
  
  A few words first
&lt;/h2&gt;

&lt;p&gt;agentic-hosting is one program, &lt;code&gt;ah&lt;/code&gt;, that runs on your server. Agents talk to it over an HTTP API, the same way they'd call any web service.&lt;/p&gt;

&lt;p&gt;Three terms come up a lot below:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A &lt;strong&gt;tenant&lt;/strong&gt; is an account on the server. Everything an agent creates (apps, databases, builds) belongs to one tenant.&lt;/li&gt;
&lt;li&gt;An &lt;strong&gt;API key&lt;/strong&gt; is the password an agent sends with every request. A tenant can have several.&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;bootstrap token&lt;/strong&gt; 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.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In the examples, &lt;code&gt;$AH_URL&lt;/code&gt; is your server's address.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: one tenant per agent
&lt;/h2&gt;

&lt;p&gt;Creating a tenant is one request with the bootstrap token in a header:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$AH_URL&lt;/span&gt;&lt;span class="s2"&gt;/v1/tenants/register"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"X-Bootstrap-Token: &lt;/span&gt;&lt;span class="nv"&gt;$AH_BOOTSTRAP_TOKEN&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"name":"blog-agent",
       "email":"me+blog@example.com"}'&lt;/span&gt;
&lt;span class="c"&gt;# {"tenant_id":"3f9a...","api_key":"8c41....d07e..."}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The server answers &lt;code&gt;201&lt;/code&gt; 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.&lt;/p&gt;

&lt;p&gt;Some rules worth knowing before you script this for a fleet of agents:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Each tenant needs its own email.&lt;/strong&gt; Two tenants can share a name but not an email. I use plus addresses (&lt;code&gt;me+blog@&lt;/code&gt;, &lt;code&gt;me+shop@&lt;/code&gt;), which the server accepts. A duplicate email gets &lt;code&gt;422&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Registration is slow on purpose.&lt;/strong&gt; 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 &lt;code&gt;429&lt;/code&gt; with a &lt;code&gt;Retry-After&lt;/code&gt; header telling you how long to wait.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The bootstrap token must be at least 32 characters.&lt;/strong&gt; The server won't start with a shorter one.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you're just trying this on a laptop, &lt;code&gt;./bin/ah --dev --open-registration&lt;/code&gt; skips the bootstrap token entirely. Anyone who can reach the port can then make an account, so keep that to your own machine.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: read the key
&lt;/h2&gt;

&lt;p&gt;A key looks like two long strings of letters and numbers joined by a dot: &lt;code&gt;keyID.secret&lt;/code&gt;. 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.&lt;/p&gt;

&lt;p&gt;Agents send the whole thing on every request:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;AH_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"8c41....d07e..."&lt;/span&gt;
curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$AH_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$AH_URL&lt;/span&gt;&lt;span class="s2"&gt;/v1/tenant"&lt;/span&gt;

&lt;span class="c"&gt;# the key's ID is everything before the dot:&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;AH_KEY&lt;/span&gt;&lt;span class="p"&gt;%%.*&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 3: keep the first key, give the agent a second
&lt;/h2&gt;

&lt;p&gt;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 &lt;code&gt;$MY_KEY&lt;/code&gt;). The agent gets a separate key that I create for it, with a name and an expiry:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$AH_URL&lt;/span&gt;&lt;span class="s2"&gt;/v1/auth/keys"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$MY_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"name":"claude-code","expires_in":2592000}'&lt;/span&gt;
&lt;span class="c"&gt;# {"id":"51be...","name":"claude-code",&lt;/span&gt;
&lt;span class="c"&gt;#  "api_key":"51be....9a2f...","prefix":"51be20c7",&lt;/span&gt;
&lt;span class="c"&gt;#  "expires_at":1795359600}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;expires_in&lt;/code&gt; 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.&lt;/p&gt;

&lt;p&gt;Now there are two keys on the tenant and I can tell them apart. (&lt;code&gt;jq&lt;/code&gt; is a small tool for picking fields out of JSON.)&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$MY_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$AH_URL&lt;/span&gt;&lt;span class="s2"&gt;/v1/auth/keys"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  | jq &lt;span class="s1"&gt;'.[] | {id, name, last_used_at, expires_at}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The list never shows secrets. &lt;code&gt;last_used_at&lt;/code&gt; 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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: swap keys without downtime
&lt;/h2&gt;

&lt;p&gt;Rotating a key is the same three moves every time:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Create a new key for the agent.&lt;/li&gt;
&lt;li&gt;Give it to the agent and let it make a request with it.&lt;/li&gt;
&lt;li&gt;Revoke the old one, using its ID (the part before the dot).
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; /dev/null &lt;span class="nt"&gt;-w&lt;/span&gt; &lt;span class="s1"&gt;'%{http_code}\n'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-X&lt;/span&gt; DELETE &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$MY_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$AH_URL&lt;/span&gt;&lt;span class="s2"&gt;/v1/auth/keys/&lt;/span&gt;&lt;span class="nv"&gt;$OLD_KEY_ID&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="c"&gt;# 204&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Revoking takes effect right away, even though the server caches logins for speed. The very next request with the old key gets &lt;code&gt;401&lt;/code&gt; and &lt;code&gt;{"error":"invalid api key"}&lt;/code&gt;. An expired key gets the same answer.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5: cut off one agent
&lt;/h2&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Revoke its key.&lt;/strong&gt; 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.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Suspend the whole tenant.&lt;/strong&gt; If you want everything that agent built stopped too:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-X&lt;/span&gt; DELETE &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$MY_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$AH_URL&lt;/span&gt;&lt;span class="s2"&gt;/v1/tenant"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;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 &lt;code&gt;AH_ADMIN_TOKEN&lt;/code&gt; when you install):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"X-Admin-Token: &lt;/span&gt;&lt;span class="nv"&gt;$AH_ADMIN_TOKEN&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$AH_URL&lt;/span&gt;&lt;span class="s2"&gt;/v1/tenants/&lt;/span&gt;&lt;span class="nv"&gt;$TENANT_ID&lt;/span&gt;&lt;span class="s2"&gt;/reactivate"&lt;/span&gt;
&lt;span class="c"&gt;# {"tenant_id":"3f9a...","api_key":"...",&lt;/span&gt;
&lt;span class="c"&gt;#  "status":"active"}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;$TENANT_ID&lt;/code&gt; is the &lt;code&gt;tenant_id&lt;/code&gt; from step 1. You get a fresh key. The databases come back on their own. The apps need a redeploy.&lt;/p&gt;

&lt;p&gt;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 &lt;code&gt;POST /v1/auth/recover&lt;/code&gt;. It's one more reason I never give the admin token to an agent.&lt;/p&gt;

&lt;h2&gt;
  
  
  What "its own space" actually means
&lt;/h2&gt;

&lt;p&gt;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 &lt;code&gt;404&lt;/code&gt; and &lt;code&gt;{"error":"service not found"}&lt;/code&gt;, as if the service doesn't exist.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$AH_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$AH_URL&lt;/span&gt;&lt;span class="s2"&gt;/v1/tenant/usage"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  | jq &lt;span class="s1"&gt;'{services, databases, api_keys, rate_limit}'&lt;/span&gt;
&lt;span class="c"&gt;# {"services":{"used":2,"max":5},&lt;/span&gt;
&lt;span class="c"&gt;#  "databases":{"used":1,"max":3},&lt;/span&gt;
&lt;span class="c"&gt;#  "api_keys":{"used":2,"max":20},&lt;/span&gt;
&lt;span class="c"&gt;#  "rate_limit":100}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As the operator, you can raise or lower a tenant's limits with &lt;code&gt;PATCH /v1/tenants/{id}/quotas&lt;/code&gt; and the admin token. Hitting a limit returns &lt;code&gt;409&lt;/code&gt; with a message like &lt;code&gt;quota exceeded: maximum 5 services reached&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The one thing that isn't per tenant: web addresses
&lt;/h2&gt;

&lt;p&gt;If you start &lt;code&gt;ah&lt;/code&gt; with a domain (&lt;code&gt;--base-domain apps.example.com&lt;/code&gt;), each app gets a web address from its name: a service called &lt;code&gt;web&lt;/code&gt; lives at &lt;code&gt;https://web.apps.example.com&lt;/code&gt;. Those names are shared across the whole server, first come, first served. If one agent already has &lt;code&gt;web&lt;/code&gt;, the next agent that tries gets:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;409 {"error":"subdomain already taken: web —
choose a different service name"}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A handful of names like &lt;code&gt;api&lt;/code&gt;, &lt;code&gt;admin&lt;/code&gt;, &lt;code&gt;www&lt;/code&gt;, and &lt;code&gt;dashboard&lt;/code&gt; are reserved for everyone. The fix is simple: have each agent put its project in the name, like &lt;code&gt;blog-web&lt;/code&gt; or &lt;code&gt;shop-api&lt;/code&gt;. Without a domain, apps get an address made from their random ID, so this never comes up.&lt;/p&gt;

&lt;h2&gt;
  
  
  Speed limits for each agent
&lt;/h2&gt;

&lt;p&gt;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 &lt;a href="https://dev.to/dennisonbertram/my-ai-agent-never-gets-tired-and-sometimes-its-wrong-heres-how-my-server-copes-oa6"&gt;an earlier post&lt;/a&gt;, so here's just the part that matters when several agents share a server.&lt;/p&gt;

&lt;p&gt;There are two different 429s, and they mean different things:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;429 {"error":"rate limit exceeded"}
    -&amp;gt; this agent is going too fast

429 {"error":"global rate limit exceeded"}
    -&amp;gt; the whole server is busy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both come with a &lt;code&gt;Retry-After&lt;/code&gt; 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 &lt;code&gt;X-RateLimit-Limit&lt;/code&gt; and &lt;code&gt;X-RateLimit-Remaining&lt;/code&gt;, so an agent can see the first kind coming.&lt;/p&gt;

&lt;h2&gt;
  
  
  Retries stay inside the tenant too
&lt;/h2&gt;

&lt;p&gt;Agents retry a lot, so the server supports an &lt;code&gt;Idempotency-Key&lt;/code&gt; 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.&lt;/p&gt;

&lt;p&gt;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 &lt;code&gt;409&lt;/code&gt; if their requests were different. Separate tenants can never collide.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I tell my agents
&lt;/h2&gt;

&lt;p&gt;If you run more than one agent, here's the short version I give each of them:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;- 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.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/dennisonbertram/agentic-hosting" rel="noopener noreferrer"&gt;Star agentic-hosting on GitHub&lt;/a&gt; so more people find it.&lt;/li&gt;
&lt;li&gt;Give it to your agent: &lt;a href="https://raw.githubusercontent.com/dennisonbertram/agentic-hosting/main/llms.txt" rel="noopener noreferrer"&gt;llms.txt&lt;/a&gt; to sign up and set up, or the &lt;a href="https://raw.githubusercontent.com/dennisonbertram/agentic-hosting/main/skills/agentic-hosting/SKILL.md" rel="noopener noreferrer"&gt;agent skill&lt;/a&gt; for daily use.&lt;/li&gt;
&lt;li&gt;Try the &lt;a href="https://dennisonbertram.github.io/agentic-hosting/docs/install/" rel="noopener noreferrer"&gt;quickstart&lt;/a&gt;, or start at the &lt;a href="https://dennisonbertram.github.io/agentic-hosting/" rel="noopener noreferrer"&gt;docs site&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/dennisonbertram/agentic-hosting/issues" rel="noopener noreferrer"&gt;Open an issue&lt;/a&gt; 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.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;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.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>devops</category>
      <category>selfhosted</category>
      <category>security</category>
    </item>
    <item>
      <title>When Your AI Agent's Host Goes Down, What Should It Do? Here's What I Tell Mine.</title>
      <dc:creator>Dennison Bertram</dc:creator>
      <pubDate>Thu, 22 Oct 2026 13:41:52 +0000</pubDate>
      <link>https://dev.to/dennisonbertram/when-your-ai-agents-host-goes-down-what-should-it-do-heres-what-i-tell-mine-5d24</link>
      <guid>https://dev.to/dennisonbertram/when-your-ai-agents-host-goes-down-what-should-it-do-heres-what-i-tell-mine-5d24</guid>
      <description>&lt;p&gt;Railway had a rough Wednesday. Builds were paused for about a day, and when they came back the status page asked people not to trigger the same deployment more than once. That's good advice for humans. An AI agent will never think of it on its own.&lt;/p&gt;

&lt;p&gt;This post covers how an agent can tell "the platform is down" from "my deploy is broken," what it should do in each case, and the exact rules I paste into my agents' instructions file.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;⭐ If this is useful, a star on &lt;a href="https://github.com/dennisonbertram/agentic-hosting" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; helps other people find it.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;🤖 &lt;strong&gt;Give this to your agent:&lt;/strong&gt; point it at &lt;a href="https://raw.githubusercontent.com/dennisonbertram/agentic-hosting/main/llms.txt" rel="noopener noreferrer"&gt;https://raw.githubusercontent.com/dennisonbertram/agentic-hosting/main/llms.txt&lt;/a&gt; and it can sign up and set things up from that one link.&lt;/p&gt;

&lt;h2&gt;
  
  
  What happened at Railway
&lt;/h2&gt;

&lt;p&gt;I'm only repeating what Railway has published. On October 21, Railway &lt;a href="https://blog.railway.com/p/security-disclosure-wednesday-october-21st" rel="noopener noreferrer"&gt;disclosed&lt;/a&gt; that someone may have accessed encrypted environment variables (the settings where apps keep API keys and database passwords) and, for some of them, gotten the decrypted contents. Railway told every customer to rotate those secrets as a precaution, and paused all deployments while it investigated.&lt;/p&gt;

&lt;p&gt;As of this morning, the &lt;a href="https://status.railway.com" rel="noopener noreferrer"&gt;status page&lt;/a&gt; says builds are back for Pro and Enterprise, the deploy queue is nearly cleared, and Railway is still working on Hobby, then Free and Trial.&lt;/p&gt;

&lt;p&gt;I'm not writing this to pile on. Every platform has a bad day, including mine. But it's a clean example of a situation most agents have no plan for.&lt;/p&gt;

&lt;h2&gt;
  
  
  The question I got
&lt;/h2&gt;

&lt;p&gt;On my &lt;a href="https://dev.to/dennisonbertram/railway-is-down-today-my-agents-taught-me-to-keep-a-second-place-to-ship-37kd"&gt;last post&lt;/a&gt;, a reader named alexcodebytes asked something I've been chewing on since. If an agent has a backup place to deploy, how does it know when to use it? Can it tell "the platform is down" from "my deploy failed for some other reason"?&lt;/p&gt;

&lt;p&gt;Left alone, mostly no. An agent sees an error and assumes it caused it. That's a reasonable guess most of the time, which is exactly why it's dangerous on the days it's wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  Check the platform before touching code
&lt;/h2&gt;

&lt;p&gt;The fix is boring: make the agent look outside before it looks inside. Before it edits a single line to "fix" a failed deploy, it should answer three questions.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Does the platform say it's down?&lt;/strong&gt; Check the status page. If there's an open incident about builds or the API, stop there.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Is it failing everywhere?&lt;/strong&gt; If a deploy of the last version that worked also fails, the problem isn't the new code.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Does the error point at my code?&lt;/strong&gt; A build log with a compile error or a failing test is yours. A timeout, a 5xx error, or "service unavailable" usually isn't.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If all three point at the platform, it's an outage. If the error points at your code and the platform looks healthy, it's a broken deploy, and the agent should fix it like normal.&lt;/p&gt;

&lt;h2&gt;
  
  
  Then stop retrying
&lt;/h2&gt;

&lt;p&gt;This is the part agents get worst. Each retry feels free to them. During an outage it just adds to the pile, which is why Railway asked people not to redeploy the same thing over and over.&lt;/p&gt;

&lt;p&gt;My rule is two tries, then stop and tell me. No loops, no "let me try one more approach."&lt;/p&gt;

&lt;h2&gt;
  
  
  Rotate secrets where they're issued
&lt;/h2&gt;

&lt;p&gt;If the incident involves leaked secrets, changing the value in the platform isn't enough. The old key still works until the company that issued it (your database host, your AI provider, your payment processor) revokes it. So the agent should list every key the app uses and I go revoke each one at its source.&lt;/p&gt;

&lt;p&gt;I keep that step for myself. I don't want an agent revoking production keys on its own.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fall back to a second place to ship
&lt;/h2&gt;

&lt;p&gt;Only after all of that does the agent use the backup. For me that's &lt;a href="https://github.com/dennisonbertram/agentic-hosting" rel="noopener noreferrer"&gt;agentic-hosting&lt;/a&gt;, an open-source program, &lt;code&gt;ah&lt;/code&gt;, that turns a Linux server I own into a place agents can deploy apps and databases over an HTTP API.&lt;/p&gt;

&lt;p&gt;Checking that the backup is up is one call, no key needed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$AH_URL&lt;/span&gt;&lt;span class="s2"&gt;/v1/system/health"&lt;/span&gt;
&lt;span class="c"&gt;# {"status":"ok"}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Setting the new secrets and kicking off a build from git looks like this (&lt;code&gt;$SVC&lt;/code&gt; is a service the agent already created there):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$AH_URL&lt;/span&gt;&lt;span class="s2"&gt;/v1/services/&lt;/span&gt;&lt;span class="nv"&gt;$SVC&lt;/span&gt;&lt;span class="s2"&gt;/env"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$AH_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"DATABASE_URL": "postgres://..."}'&lt;/span&gt;

&lt;span class="nv"&gt;BODY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;jq &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="nt"&gt;--arg&lt;/span&gt; sha &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;git rev-parse HEAD&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s1"&gt;'{
  source_type: "git",
  source_url: "https://github.com/you/app",
  source_ref: $sha }'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;

curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$AH_URL&lt;/span&gt;&lt;span class="s2"&gt;/v1/services/&lt;/span&gt;&lt;span class="nv"&gt;$SVC&lt;/span&gt;&lt;span class="s2"&gt;/builds"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$AH_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$BODY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The build call answers with a &lt;code&gt;build_id&lt;/code&gt; the agent can use to watch the build log.&lt;/p&gt;

&lt;h2&gt;
  
  
  The rules I paste in
&lt;/h2&gt;

&lt;p&gt;Here's the block that lives in my agents' instructions file (&lt;code&gt;CLAUDE.md&lt;/code&gt;, &lt;code&gt;AGENTS.md&lt;/code&gt;, whatever yours uses):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;When a deploy fails:
1. Read the build log. If it shows a compile
   error or failing test, fix the code.
2. Otherwise check the platform status page
   before changing any code.
3. If there is an open incident, or the last
   good version also fails to deploy, it is
   an outage. Do not edit code.
4. Retry at most twice. Then stop and tell me.
5. If the incident mentions leaked secrets,
   list every key the app uses so I can
   revoke each one where it was issued.
6. Only then, deploy to $AH_URL. Check
   $AH_URL/v1/system/health first. Tell me
   you switched and why.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It's not clever. It just writes down what a careful person already does.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this costs
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;You own the backup's uptime.&lt;/strong&gt; If your box is down at 3 AM, there's no status page. There's you.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Two places means two setups.&lt;/strong&gt; Secrets, domains, and databases have to exist in both, and data doesn't move itself.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The rules can still be wrong.&lt;/strong&gt; A status page can lag, and an agent can still guess badly. This makes good calls more likely. It doesn't make them certain.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;When a platform goes down, the risk isn't just downtime. It's an agent retrying into a wall and "fixing" code that was fine. Teach it to check the platform first, stop after two tries, leave secret rotation to you, and only then ship somewhere else. agentic-hosting is one way to have that somewhere else on a server you own.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/dennisonbertram/agentic-hosting" rel="noopener noreferrer"&gt;Star agentic-hosting on GitHub&lt;/a&gt; so more people find it.&lt;/li&gt;
&lt;li&gt;Give it to your agent: &lt;a href="https://raw.githubusercontent.com/dennisonbertram/agentic-hosting/main/llms.txt" rel="noopener noreferrer"&gt;llms.txt&lt;/a&gt; to sign up and set up, or the &lt;a href="https://raw.githubusercontent.com/dennisonbertram/agentic-hosting/main/skills/agentic-hosting/SKILL.md" rel="noopener noreferrer"&gt;agent skill&lt;/a&gt; for daily use.&lt;/li&gt;
&lt;li&gt;Set up your own backup with the &lt;a href="https://dennisonbertram.github.io/agentic-hosting/docs/install/" rel="noopener noreferrer"&gt;quickstart&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>devops</category>
      <category>selfhosted</category>
      <category>agents</category>
    </item>
    <item>
      <title>Railway Is Down Today. My Agents Taught Me to Keep a Second Place to Ship.</title>
      <dc:creator>Dennison Bertram</dc:creator>
      <pubDate>Wed, 21 Oct 2026 19:14:53 +0000</pubDate>
      <link>https://dev.to/dennisonbertram/railway-is-down-today-my-agents-taught-me-to-keep-a-second-place-to-ship-37kd</link>
      <guid>https://dev.to/dennisonbertram/railway-is-down-today-my-agents-taught-me-to-keep-a-second-place-to-ship-37kd</guid>
      <description>&lt;p&gt;&lt;strong&gt;Update (Oct 22, 8:00 AM ET):&lt;/strong&gt; Railway has since said this was a &lt;a href="https://blog.railway.com/p/security-disclosure-wednesday-october-21st" rel="noopener noreferrer"&gt;security incident&lt;/a&gt;: an unauthorized party may have accessed customer environment variables, some decrypted. They recommend rotating every secret stored there. Builds are back for Pro and Enterprise; Hobby, Free and Trial are still waiting, per the &lt;a href="https://status.railway.com" rel="noopener noreferrer"&gt;status page&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Railway has had builds and deploys paused since about 3:24 AM Eastern today. As I write this, a little after 3 PM, the &lt;a href="https://status.railway.com" rel="noopener noreferrer"&gt;status page&lt;/a&gt; still says the dashboard and API are affected and no new builds are going out in any region.&lt;/p&gt;

&lt;p&gt;I'm not writing this to dunk on Railway. I like Railway, and every platform has a day like this, including the one I run. But today is a good, concrete look at something I've been thinking about for months: what happens to your AI agents when the place they deploy to goes away for a day.&lt;/p&gt;

&lt;p&gt;This post covers what the outage looks like from an agent's side, why I keep a deploy target on a server I own, and the honest cost of doing that.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;⭐ If this is useful, a star on &lt;a href="https://github.com/dennisonbertram/agentic-hosting" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; helps other people find it.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;🤖 &lt;strong&gt;Give this to your agent:&lt;/strong&gt; point it at &lt;a href="https://raw.githubusercontent.com/dennisonbertram/agentic-hosting/main/llms.txt" rel="noopener noreferrer"&gt;https://raw.githubusercontent.com/dennisonbertram/agentic-hosting/main/llms.txt&lt;/a&gt; and it can sign up and set things up from that one link.&lt;/p&gt;

&lt;h2&gt;
  
  
  What happened, from the status page
&lt;/h2&gt;

&lt;p&gt;Here's what Railway has posted so far. I'm only repeating what's on &lt;a href="https://status.railway.com" rel="noopener noreferrer"&gt;status.railway.com&lt;/a&gt;; times are converted to Eastern.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;3:24 AM:&lt;/strong&gt; Railway starts investigating an issue.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;5:28 AM:&lt;/strong&gt; Issue identified. The dashboard, API and CLI are unavailable, and new builds are paused in all regions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;7:33 AM:&lt;/strong&gt; The API is partly back, but builds are still paused.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;12:29 PM onward:&lt;/strong&gt; Builds and deploys stay paused, the API and dashboard are still affected, and the support site (station.railway.com) is down.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;2:45 PM:&lt;/strong&gt; Latest update, with no new information. Railway says it will keep posting at least every 30 minutes.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The incident is titled "Dashboard and API unavailable, builds paused" and lists builds in US West, EU West, Southeast Asia and US East as affected. Check the status page for the current state, since it'll be out of date by the time you read this.&lt;/p&gt;

&lt;h2&gt;
  
  
  What an agent does with a paused platform
&lt;/h2&gt;

&lt;p&gt;When I'm the one deploying, an outage is annoying. I read the status page, sigh, and go do something else.&lt;/p&gt;

&lt;p&gt;An agent doesn't sigh. It pushes a fix, asks for a deploy, gets an error, and tries again. Maybe it reads the error and decides its own code is broken, and starts "fixing" things that weren't wrong. If it's running overnight, you wake up to a long log of retries and nothing shipped.&lt;/p&gt;

&lt;p&gt;That's not the agent's fault. It was handed one place to deploy, and that place is closed. There's no Plan B in its instructions, because there's no Plan B in the setup.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I keep a box of my own
&lt;/h2&gt;

&lt;p&gt;This is the reason I built &lt;a href="https://github.com/dennisonbertram/agentic-hosting" rel="noopener noreferrer"&gt;agentic-hosting&lt;/a&gt;. It's open source, and it turns a Linux server you own into a place your agents can deploy apps and databases by themselves, with limits you set.&lt;/p&gt;

&lt;p&gt;It's one program, &lt;code&gt;ah&lt;/code&gt;, that takes requests over an HTTP API. So the "second place to ship" is just a URL and a key your agent already knows how to use. A deploy from a git repo looks like this (&lt;code&gt;$SVC&lt;/code&gt; is a service the agent created first):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;BODY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;jq &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="nt"&gt;--arg&lt;/span&gt; sha &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;git rev-parse HEAD&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s1"&gt;'{
  source_type: "git",
  source_url: "https://github.com/you/app",
  source_ref: $sha }'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;

curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$AH_URL&lt;/span&gt;&lt;span class="s2"&gt;/v1/services/&lt;/span&gt;&lt;span class="nv"&gt;$SVC&lt;/span&gt;&lt;span class="s2"&gt;/builds"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$AH_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$BODY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I'm not saying move everything off Railway today. I'm saying it's worth having somewhere your agents can still ship when your main platform can't. You can add one line to their instructions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;If the main platform's deploys are failing and its
status page shows an incident, deploy to $AH_URL
instead and tell me you did.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What it costs you
&lt;/h2&gt;

&lt;p&gt;Running your own deploy target isn't free, and I'd be lying if I said it was.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;You own the uptime.&lt;/strong&gt; If your box goes down at 3 AM, there's no status page and no team working on it. There's you.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You do the upkeep.&lt;/strong&gt; Updates, disk space, backups and TLS are your job. agentic-hosting handles a lot of the guardrails, but it's still a server.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One box is one box.&lt;/strong&gt; A single server is also a single point of failure. The point is having two places that fail for different reasons, not one place that never fails.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For a lot of people, a managed platform is the right main choice. I think the mistake is having exactly one place your agents know how to ship.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;Railway's builds have been paused for most of today, and anyone whose agents deploy only there is waiting along with them. Outages happen everywhere. What you can control is whether your agents have a second place to go. agentic-hosting gives them one on a server you own, and the tradeoff is that keeping it up is on you.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/dennisonbertram/agentic-hosting" rel="noopener noreferrer"&gt;Star agentic-hosting on GitHub&lt;/a&gt; so more people find it.&lt;/li&gt;
&lt;li&gt;Give it to your agent: &lt;a href="https://raw.githubusercontent.com/dennisonbertram/agentic-hosting/main/llms.txt" rel="noopener noreferrer"&gt;llms.txt&lt;/a&gt; to sign up and set up, or the &lt;a href="https://raw.githubusercontent.com/dennisonbertram/agentic-hosting/main/skills/agentic-hosting/SKILL.md" rel="noopener noreferrer"&gt;agent skill&lt;/a&gt; for daily use.&lt;/li&gt;
&lt;li&gt;Try the &lt;a href="https://dennisonbertram.github.io/agentic-hosting/docs/install/" rel="noopener noreferrer"&gt;quickstart&lt;/a&gt;, or start at the &lt;a href="https://dennisonbertram.github.io/agentic-hosting/" rel="noopener noreferrer"&gt;docs site&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>devops</category>
      <category>selfhosted</category>
      <category>railway</category>
    </item>
    <item>
      <title>I Give My AI Agent a Git Link and Get Back a Live App. Here's What It Does When the Build Breaks.</title>
      <dc:creator>Dennison Bertram</dc:creator>
      <pubDate>Wed, 21 Oct 2026 14:15:53 +0000</pubDate>
      <link>https://dev.to/dennisonbertram/i-give-my-ai-agent-a-git-link-and-get-back-a-live-app-heres-what-it-does-when-the-build-breaks-466a</link>
      <guid>https://dev.to/dennisonbertram/i-give-my-ai-agent-a-git-link-and-get-back-a-live-app-heres-what-it-does-when-the-build-breaks-466a</guid>
      <description>&lt;p&gt;Here's how I used to deploy a side project. Push the code. Open the hosting dashboard. Watch the build scroll by. See it go red. Squint at the error, fix one line, push again. Wait. Usually it works the second time.&lt;/p&gt;

&lt;p&gt;Every step in that loop is something a coding agent can do, except one: watching the build. On most hosting tools the build output lives on a web page made for a person to look at. My agent can write the fix in ten seconds. It just can't see what broke.&lt;/p&gt;

&lt;p&gt;This post walks through how &lt;a href="https://github.com/dennisonbertram/agentic-hosting" rel="noopener noreferrer"&gt;agentic-hosting&lt;/a&gt;, an open-source tool that turns a Linux server into a place where AI agents can deploy apps on their own, closes that gap. Your agent hands the server a git link, reads the build output as it happens, fixes what broke, and comes back with a live URL. Every step is one HTTP request, and I'll show each one.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;⭐ If this is useful, a star on &lt;a href="https://github.com/dennisonbertram/agentic-hosting" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; helps other people find it.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;🤖 &lt;strong&gt;Give this to your agent:&lt;/strong&gt; point it at &lt;a href="https://raw.githubusercontent.com/dennisonbertram/agentic-hosting/main/llms.txt" rel="noopener noreferrer"&gt;https://raw.githubusercontent.com/dennisonbertram/agentic-hosting/main/llms.txt&lt;/a&gt; and it can sign up and set things up from that one link.&lt;/p&gt;

&lt;h2&gt;
  
  
  The loop
&lt;/h2&gt;

&lt;p&gt;agentic-hosting is a single program called &lt;code&gt;ah&lt;/code&gt; that runs on your server. Agents talk to it over an HTTP API. In the examples below, &lt;code&gt;$AH_URL&lt;/code&gt; is your server's address and &lt;code&gt;$AH_KEY&lt;/code&gt; is the agent's API key.&lt;/p&gt;

&lt;p&gt;The loop your agent runs looks like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Create a service, the server's name for one app.&lt;/li&gt;
&lt;li&gt;Start a build from a git URL.&lt;/li&gt;
&lt;li&gt;Stream the build output.&lt;/li&gt;
&lt;li&gt;If it fails, read the end of the log.&lt;/li&gt;
&lt;li&gt;Fix the code, push, and build again.&lt;/li&gt;
&lt;li&gt;When it's running, check the URL.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Step 1: make a place for the app
&lt;/h2&gt;

&lt;p&gt;A service needs an &lt;code&gt;image&lt;/code&gt; when you create it. That's the packaged app Docker runs. For a git deploy there's no image yet, so send the placeholder &lt;code&gt;"pending"&lt;/code&gt;, which is what agentic-hosting's own command-line tool sends:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$AH_URL&lt;/span&gt;&lt;span class="s2"&gt;/v1/services"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$AH_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"name":"web","image":"pending","port":3000}'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  | jq &lt;span class="s1"&gt;'{id, status, url}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Save the &lt;code&gt;id&lt;/code&gt; as &lt;code&gt;$SVC&lt;/code&gt;. The response already has the address the app will live at. If your server was started with a base domain like &lt;code&gt;apps.example.com&lt;/code&gt;, that's &lt;code&gt;https://web.apps.example.com&lt;/code&gt;. Without one, it's &lt;code&gt;http://&amp;lt;service-id&amp;gt;.localhost&lt;/code&gt;, which only works on the server itself.&lt;/p&gt;

&lt;p&gt;One thing that looks scary and isn't: creating a service always kicks off a deploy, and a deploy of an image called &lt;code&gt;pending&lt;/code&gt; fails. So the service will show &lt;code&gt;failed&lt;/code&gt; until the first build lands. The build replaces the image, so your agent can ignore that.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;port&lt;/code&gt; is the port your app listens on. It defaults to 8000. I'll come back to it, because it's the most common reason a "running" app doesn't answer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: hand it the git link
&lt;/h2&gt;

&lt;p&gt;Now the build. agentic-hosting uses &lt;a href="https://nixpacks.com" rel="noopener noreferrer"&gt;Nixpacks&lt;/a&gt;, an open-source tool that looks at a repo, works out what kind of app it is (a &lt;code&gt;package.json&lt;/code&gt; means Node, a &lt;code&gt;requirements.txt&lt;/code&gt; means Python, and so on), and builds an image without you writing a Dockerfile. That matters for an agent, because it's one less file it has to get right.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;BODY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;jq &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="nt"&gt;--arg&lt;/span&gt; sha &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;git rev-parse HEAD&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s1"&gt;'{
  source_type: "git",
  source_url: "https://github.com/you/app",
  source_ref: $sha }'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;

curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$AH_URL&lt;/span&gt;&lt;span class="s2"&gt;/v1/services/&lt;/span&gt;&lt;span class="nv"&gt;$SVC&lt;/span&gt;&lt;span class="s2"&gt;/builds"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$AH_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$BODY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="c"&gt;# {"build_id":"9f2c...",&lt;/span&gt;
&lt;span class="c"&gt;#  "image":"127.0.0.1:5000/ah/...",&lt;/span&gt;
&lt;span class="c"&gt;#  "status":"pending"}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The server answers &lt;code&gt;202&lt;/code&gt; right away and does the work in the background. Save &lt;code&gt;build_id&lt;/code&gt; as &lt;code&gt;$BUILD&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;A few rules about the URL:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;HTTPS only&lt;/strong&gt;, and no username or token in it. In practice that means public repos for now.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Known hosts only:&lt;/strong&gt; github.com, gitlab.com, bitbucket.org, and codeberg.org. This stops someone from pointing the server at a host that quietly resolves to an internal address.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;source_ref&lt;/code&gt;&lt;/strong&gt; can be a branch, a tag, or a commit SHA. It defaults to &lt;code&gt;main&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I have my agent send the commit SHA. Then it knows exactly which code it's looking at when it reads the log, and a push from someone else in the meantime can't change what got built. The trade-off is that a SHA means a full clone instead of a quick shallow one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: watch the build
&lt;/h2&gt;

&lt;p&gt;This is the step most hosting tools keep on a web page. Here it's a plain-text stream:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-sN&lt;/span&gt; &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$AH_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$AH_URL&lt;/span&gt;&lt;span class="s2"&gt;/v1/services/&lt;/span&gt;&lt;span class="nv"&gt;$SVC&lt;/span&gt;&lt;span class="s2"&gt;/builds/&lt;/span&gt;&lt;span class="nv"&gt;$BUILD&lt;/span&gt;&lt;span class="s2"&gt;/logs?follow=true"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;-N&lt;/code&gt; tells curl to print lines as they arrive. The stream starts from the top of the log, so it's fine to connect late, and it closes when the build and the deploy after it are done. Lines from &lt;code&gt;ah&lt;/code&gt; itself start with &lt;code&gt;[ah]&lt;/code&gt;. Everything else is the raw output from git, Nixpacks, and your app's install and build commands.&lt;/p&gt;

&lt;p&gt;Two things to know. If the build goes quiet for a minute, say during a long install, the connection can drop. Reconnecting is safe because the stream replays from the start. And each build log is capped at 5 MB. Past that, the server keeps the last 500 lines, which is the part with the error anyway.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: read the failure
&lt;/h2&gt;

&lt;p&gt;Here's a failure I see a lot from agents. It added a package to &lt;code&gt;package.json&lt;/code&gt; and never updated &lt;code&gt;package-lock.json&lt;/code&gt;. On its laptop that's fine. Nixpacks runs &lt;code&gt;npm ci&lt;/code&gt;, which refuses to install when the two files disagree. The end of the log looks something like this (trimmed):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;npm error `npm ci` can only install packages when
npm error your package.json and package-lock.json
npm error are in sync. Please update your lock file
npm error with `npm install` before continuing.
...
[ah] Build failed: nixpacks build: nixpacks build failed
[ah] BUILD FAILED: build worker failed (...)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The agent doesn't need to watch the stream to find this. Once the build is done, the same endpoint without &lt;code&gt;follow&lt;/code&gt; returns the whole log, and the build's status says how it ended:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$AH_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$AH_URL&lt;/span&gt;&lt;span class="s2"&gt;/v1/services/&lt;/span&gt;&lt;span class="nv"&gt;$SVC&lt;/span&gt;&lt;span class="s2"&gt;/builds/&lt;/span&gt;&lt;span class="nv"&gt;$BUILD&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | jq &lt;span class="nt"&gt;-r&lt;/span&gt; .status
&lt;span class="c"&gt;# failed&lt;/span&gt;

curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$AH_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$AH_URL&lt;/span&gt;&lt;span class="s2"&gt;/v1/services/&lt;/span&gt;&lt;span class="nv"&gt;$SVC&lt;/span&gt;&lt;span class="s2"&gt;/builds/&lt;/span&gt;&lt;span class="nv"&gt;$BUILD&lt;/span&gt;&lt;span class="s2"&gt;/logs"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  | &lt;span class="nb"&gt;tail&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; 40
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;tail&lt;/code&gt; matters more than it looks. A Node build can print thousands of lines, and the error is almost always near the bottom. Handing the model the last 40 lines instead of the whole log keeps it focused on what broke.&lt;/p&gt;

&lt;p&gt;A status of &lt;code&gt;succeeded&lt;/code&gt; means the image built and was deployed. &lt;code&gt;failed&lt;/code&gt; covers everything else, including a build your agent cancelled.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5: fix it and build again
&lt;/h2&gt;

&lt;p&gt;The agent does what you'd do. It runs &lt;code&gt;npm install&lt;/code&gt;, commits the updated lockfile, and pushes. Then it starts a new build with the new SHA. Same request as step 2. Every build gets its own ID and its own image, so the agent can compare the logs of the build that failed and the one that worked.&lt;/p&gt;

&lt;p&gt;If the app was already live from an earlier build, a failed build doesn't touch it. The new image only replaces the running one after it builds cleanly. (If the new version then fails to start, the old one is already gone, which is one more reason to check the URL at the end.)&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 6: check the URL
&lt;/h2&gt;

&lt;p&gt;When the stream ends with &lt;code&gt;[ah] Deploy succeeded&lt;/code&gt;, look at the service:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$AH_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$AH_URL&lt;/span&gt;&lt;span class="s2"&gt;/v1/services/&lt;/span&gt;&lt;span class="nv"&gt;$SVC&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  | jq &lt;span class="s1"&gt;'{status, url, last_error}'&lt;/span&gt;
&lt;span class="c"&gt;# {"status": "running",&lt;/span&gt;
&lt;span class="c"&gt;#  "url": "https://web.apps.example.com", ...}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then have the agent actually request that URL. &lt;code&gt;running&lt;/code&gt; means the container started. It doesn't mean the app answers.&lt;/p&gt;

&lt;p&gt;The usual reason it doesn't is the port. The server sends traffic to the service's port, which is 8000 unless you set it, or to the value of a &lt;code&gt;PORT&lt;/code&gt; environment variable if the service has one. It doesn't set &lt;code&gt;PORT&lt;/code&gt; inside the container for you. So if the app listens on 3000 and nobody said so, the container runs fine and every request fails.&lt;/p&gt;

&lt;p&gt;The fix doesn't need a new build. Set &lt;code&gt;PORT&lt;/code&gt; (most frameworks read it, and the server uses it for routing too), then redeploy, which restarts the same image with the new settings:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$AH_URL&lt;/span&gt;&lt;span class="s2"&gt;/v1/services/&lt;/span&gt;&lt;span class="nv"&gt;$SVC&lt;/span&gt;&lt;span class="s2"&gt;/env"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$AH_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"PORT":"3000"}'&lt;/span&gt;

curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$AH_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$AH_URL&lt;/span&gt;&lt;span class="s2"&gt;/v1/services/&lt;/span&gt;&lt;span class="nv"&gt;$SVC&lt;/span&gt;&lt;span class="s2"&gt;/redeploy"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Redeploy doesn't pull new code. For a code change, start a new build.&lt;/p&gt;

&lt;h2&gt;
  
  
  Things that trip agents up
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;One build at a time per agent.&lt;/strong&gt; If your agent starts a second build while one is running, the second one fails right away with "a build is already running for this tenant" in its log. Wait for the first to finish.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Builds have time limits.&lt;/strong&gt; 5 minutes to clone, 10 for the Nixpacks build, 5 to store the image, and 20 for the whole thing. Nixpacks runs with its cache turned off, so plan on every build installing dependencies again.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cancelling.&lt;/strong&gt; &lt;code&gt;DELETE /v1/services/$SVC/builds/$BUILD&lt;/code&gt; answers &lt;code&gt;{"status":"cancelled"}&lt;/code&gt;, but the build itself is then marked &lt;code&gt;failed&lt;/code&gt;, with "Build cancelled by user" at the end of its log. Once a build has moved on to deploying, cancelling returns &lt;code&gt;409&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A full disk says so.&lt;/strong&gt; If the server's disk is over 90% full, starting a build returns &lt;code&gt;503&lt;/code&gt; with a &lt;code&gt;Retry-After&lt;/code&gt; header. I wrote about that and the other limits in &lt;a href="https://dev.to/dennisonbertram/my-ai-agent-never-gets-tired-and-sometimes-its-wrong-heres-how-my-server-copes-oa6"&gt;my last post&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What I tell my agent
&lt;/h2&gt;

&lt;p&gt;If you write your own instructions for an agent that deploys this way, here's the short version:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;- Create the service with image "pending" and the
  port your app listens on.
- Build from a pushed commit SHA, not a branch.
- Stream logs with ?follow=true. If the stream
  drops, reconnect; it replays from the start.
- On failure, read the last 40 lines of the log,
  fix the code, push, and start a new build.
- When the build succeeds, request the URL.
  If it doesn't answer, check the port.
- Wrong port or env: set env, then redeploy.
  Code change: new build.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;The deploy loop is simple: build, read the error, fix, build again. The only thing an agent was missing was the error. agentic-hosting gives it the build output as plain text, a status it can check, and a URL at the end, so the whole loop runs without anyone opening a dashboard.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/dennisonbertram/agentic-hosting" rel="noopener noreferrer"&gt;Star agentic-hosting on GitHub&lt;/a&gt; so more people find it.&lt;/li&gt;
&lt;li&gt;Give it to your agent: &lt;a href="https://raw.githubusercontent.com/dennisonbertram/agentic-hosting/main/llms.txt" rel="noopener noreferrer"&gt;llms.txt&lt;/a&gt; to sign up and set up, or the &lt;a href="https://raw.githubusercontent.com/dennisonbertram/agentic-hosting/main/skills/agentic-hosting/SKILL.md" rel="noopener noreferrer"&gt;agent skill&lt;/a&gt; for daily use.&lt;/li&gt;
&lt;li&gt;Try the &lt;a href="https://dennisonbertram.github.io/agentic-hosting/docs/install/" rel="noopener noreferrer"&gt;quickstart&lt;/a&gt;, or start at the &lt;a href="https://dennisonbertram.github.io/agentic-hosting/" rel="noopener noreferrer"&gt;docs site&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/dennisonbertram/agentic-hosting/issues" rel="noopener noreferrer"&gt;Open an issue&lt;/a&gt; if your agent hits a build failure it can't read its way out of. I want to see the log.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>devops</category>
      <category>selfhosted</category>
      <category>docker</category>
    </item>
    <item>
      <title>My AI Agent Never Gets Tired, and Sometimes It's Wrong. Here's How My Server Copes.</title>
      <dc:creator>Dennison Bertram</dc:creator>
      <pubDate>Mon, 19 Oct 2026 14:23:02 +0000</pubDate>
      <link>https://dev.to/dennisonbertram/my-ai-agent-never-gets-tired-and-sometimes-its-wrong-heres-how-my-server-copes-oa6</link>
      <guid>https://dev.to/dennisonbertram/my-ai-agent-never-gets-tired-and-sometimes-its-wrong-heres-how-my-server-copes-oa6</guid>
      <description>&lt;p&gt;My coding agent doesn't get tired, and it doesn't get embarrassed. If a request fails, it sends it again. If its app crashes on startup, it redeploys and watches it crash again. A person would stop after the third try and go read the logs. An agent with a loop and an API key can keep going all night.&lt;/p&gt;

&lt;p&gt;So when I built a server for agents to deploy to, I assumed the user would be tireless and sometimes wrong. I put the limits in the server itself, because I can't count on every agent to behave.&lt;/p&gt;

&lt;p&gt;This post walks through four of those limits in &lt;a href="https://github.com/dennisonbertram/agentic-hosting" rel="noopener noreferrer"&gt;agentic-hosting&lt;/a&gt;, an open-source tool that turns a Linux server into a place where AI agents can deploy apps and databases on their own. For each one, I'll show what your agent sees when it kicks in and the requests that get things moving again.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;⭐ If this is useful, a star on &lt;a href="https://github.com/dennisonbertram/agentic-hosting" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; helps other people find it.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;🤖 &lt;strong&gt;Give this to your agent:&lt;/strong&gt; point it at &lt;a href="https://raw.githubusercontent.com/dennisonbertram/agentic-hosting/main/llms.txt" rel="noopener noreferrer"&gt;https://raw.githubusercontent.com/dennisonbertram/agentic-hosting/main/llms.txt&lt;/a&gt; and it can sign up and set things up from that one link.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why an agent needs different limits
&lt;/h2&gt;

&lt;p&gt;Most hosting tools assume a person is on the other end. A person clicks deploy once, sees the error, and goes to fix it. An agent has different habits. It retries anything that looks like it failed, and it can fire off a lot of requests in a few seconds. Those are good habits most of the time. They go wrong in four places: requests that get done twice, apps stuck in a crash loop, a disk that fills up, and too many requests at once.&lt;/p&gt;

&lt;p&gt;agentic-hosting is a single program called &lt;code&gt;ah&lt;/code&gt; that runs on your server. Agents talk to it over an HTTP API. In the examples below, &lt;code&gt;$AH_URL&lt;/code&gt; is your server's address and &lt;code&gt;$AH_KEY&lt;/code&gt; is the agent's API key.&lt;/p&gt;

&lt;h2&gt;
  
  
  Retries that don't do the work twice
&lt;/h2&gt;

&lt;p&gt;Say the agent asks for a Postgres database. The server creates it, but the response never makes it back. Maybe the connection dropped, maybe a proxy in between gave up. From the agent's side the request failed, so it asks again, and now there are two databases. Database names don't have to be unique, and each agent can have three databases by default, so a couple of lost responses can use up its whole quota.&lt;/p&gt;

&lt;p&gt;The fix is an idempotency key. It's a unique string the agent makes up and sends in a header (&lt;code&gt;uuidgen&lt;/code&gt; below just prints a random ID). The server remembers each key it has seen along with the answer it gave. If the same key shows up again, the server sends back the saved answer instead of doing the work a second time.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;IDEM&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;uuidgen&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nv"&gt;BODY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'{"name":"appdb","type":"postgres"}'&lt;/span&gt;

curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$AH_URL&lt;/span&gt;&lt;span class="s2"&gt;/v1/databases"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$AH_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Idempotency-Key: &lt;/span&gt;&lt;span class="nv"&gt;$IDEM&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$BODY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Send the same request again within ten minutes, with the same key and body, and you get the first response back, with the same database ID, plus a header that says it was a replay:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-si&lt;/span&gt; &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$AH_URL&lt;/span&gt;&lt;span class="s2"&gt;/v1/databases"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$AH_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Idempotency-Key: &lt;/span&gt;&lt;span class="nv"&gt;$IDEM&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$BODY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="s1"&gt;'^idempotency-replayed'&lt;/span&gt;
&lt;span class="c"&gt;# Idempotency-Replayed: true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A few rules are worth knowing, because they decide whether a retry is safe:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Keys last 10 minutes.&lt;/strong&gt; After that the server forgets them. It also keeps them only in memory, so restarting &lt;code&gt;ah&lt;/code&gt; clears them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The key, path, and body have to match.&lt;/strong&gt; A key only counts for the same agent and the same URL. The server also stores a fingerprint of the request body with the key. Same key with a different body gets &lt;code&gt;409&lt;/code&gt; and &lt;code&gt;{"error":"idempotency key reused with different request body"}&lt;/code&gt;. Build the body once and reuse it, as above, instead of rebuilding the JSON on each try.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Only successes are saved.&lt;/strong&gt; If the first request returned an error, a retry with the same key runs again. That way one temporary failure doesn't get stuck as the permanent answer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A retry that arrives early waits.&lt;/strong&gt; If the first request is still running, the second one waits for it and gets the same answer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It covers POST requests.&lt;/strong&gt; That's every create and action route. Deletes and edits aren't covered, and they don't need to be, because doing them twice leaves things the same as doing them once. Creating an API key is left out on purpose, since the secret should be shown only once.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Keys can be up to 128 characters. A UUID per intended action is the simplest choice. Make a new key when you mean to do something new, and reuse it only when you're retrying.&lt;/p&gt;

&lt;h2&gt;
  
  
  Crash loops that stop on their own
&lt;/h2&gt;

&lt;p&gt;When an app crashes, Docker (the tool that runs each app in its own container) starts it again. That's the right call for a crash every few days. It's the wrong call for an app that crashes the moment it starts, because Docker will keep restarting it all night, and the agent watching it may keep redeploying it too.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;ah&lt;/code&gt; checks running services every 30 seconds and counts restarts. A failed health check counts as a crash too. If a service crashes 5 times within 10 minutes, &lt;code&gt;ah&lt;/code&gt; trips a circuit breaker. The name comes from the switch in your fuse box that cuts power when something's wrong. Here, &lt;code&gt;ah&lt;/code&gt; stops the container, removes it, and refuses to start it again until someone deals with it.&lt;/p&gt;

&lt;p&gt;Your agent sees it on the service itself (this uses &lt;code&gt;jq&lt;/code&gt; to pick out the fields):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$AH_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$AH_URL&lt;/span&gt;&lt;span class="s2"&gt;/v1/services/&lt;/span&gt;&lt;span class="nv"&gt;$SERVICE_ID&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  | jq &lt;span class="s1"&gt;'{status, crash_count, circuit_open, last_error}'&lt;/span&gt;
&lt;span class="c"&gt;# {&lt;/span&gt;
&lt;span class="c"&gt;#   "status": "crashed",&lt;/span&gt;
&lt;span class="c"&gt;#   "crash_count": 5,&lt;/span&gt;
&lt;span class="c"&gt;#   "circuit_open": true,&lt;/span&gt;
&lt;span class="c"&gt;#   "last_error": "container restarted 1 time(s) by ..."&lt;/span&gt;
&lt;span class="c"&gt;# }&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;While the breaker is open, start, restart, and redeploy all return &lt;code&gt;409&lt;/code&gt; with this message:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;circuit breaker is open: service has crashed too many times; use POST /reset to clear&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;There's a catch. Because the container is gone, the logs are gone with it, and &lt;code&gt;GET /v1/services/{id}/logs&lt;/code&gt; returns &lt;code&gt;409&lt;/code&gt; with "service has no container". So the best time to read logs is before the breaker trips, while the status says &lt;code&gt;crashed&lt;/code&gt;. After it trips, the clues left are &lt;code&gt;last_error&lt;/code&gt; and the deployment history, which records every crash with its exit code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$AH_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$AH_URL&lt;/span&gt;&lt;span class="s2"&gt;/v1/services/&lt;/span&gt;&lt;span class="nv"&gt;$SERVICE_ID&lt;/span&gt;&lt;span class="s2"&gt;/deployments"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  | jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'.[] | "\(.status)  \(.error_message)"'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once the agent has a fix, it resets the breaker and deploys again:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$AH_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$AH_URL&lt;/span&gt;&lt;span class="s2"&gt;/v1/services/&lt;/span&gt;&lt;span class="nv"&gt;$SERVICE_ID&lt;/span&gt;&lt;span class="s2"&gt;/reset"&lt;/span&gt;
&lt;span class="c"&gt;# {"status":"circuit breaker reset"}&lt;/span&gt;

curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$AH_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$AH_URL&lt;/span&gt;&lt;span class="s2"&gt;/v1/services/&lt;/span&gt;&lt;span class="nv"&gt;$SERVICE_ID&lt;/span&gt;&lt;span class="s2"&gt;/redeploy"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Reset leaves the service stopped with no container, so use redeploy, not start. Redeploy runs the same image (the packaged app that Docker runs) with the current environment variables, which is right when the fix was a missing variable. If the fix was in the code, reset first and then start a new build (turning the updated source code into a new image) with &lt;code&gt;POST /v1/services/{id}/builds&lt;/code&gt;, because a build can't deploy while the breaker is open.&lt;/p&gt;

&lt;p&gt;If nobody resets it, &lt;code&gt;ah&lt;/code&gt; closes the breaker by itself after 30 minutes and leaves the service stopped. Nothing restarts the app automatically. If the same service trips again, the wait becomes 1 hour, and then 4 hours for every trip after that. That count doesn't go back down after a manual reset, so a service that keeps breaking gets treated with more suspicion over time.&lt;/p&gt;

&lt;h2&gt;
  
  
  A disk that says no before it's full
&lt;/h2&gt;

&lt;p&gt;Agents build a lot. Every build leaves an image behind, databases need space, and a full disk breaks everything on the server at once, including the tools you'd use to clean it up.&lt;/p&gt;

&lt;p&gt;So before every deploy, build, and new database, &lt;code&gt;ah&lt;/code&gt; checks how full two folders are: &lt;code&gt;/var/lib/ah&lt;/code&gt;, where it keeps its own data, and &lt;code&gt;/var/lib/docker&lt;/code&gt;, where images and containers live. These thresholds are often called watermarks. At 80% used, &lt;code&gt;ah&lt;/code&gt; writes a warning to its log and carries on. At 90%, it refuses the work.&lt;/p&gt;

&lt;p&gt;What your agent sees depends on the request. Creating a service is accepted right away and deployed in the background, so the deploy fails later and the reason shows up on the service:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$AH_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$AH_URL&lt;/span&gt;&lt;span class="s2"&gt;/v1/services/&lt;/span&gt;&lt;span class="nv"&gt;$SERVICE_ID&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | jq &lt;span class="nt"&gt;-r&lt;/span&gt; .last_error
&lt;span class="c"&gt;# /var/lib/docker: insufficient disk space at&lt;/span&gt;
&lt;span class="c"&gt;# /var/lib/docker (91.4% used, threshold 90%)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Builds, new databases, restarts, and redeploys check the disk while the request is still open. Those come back as &lt;code&gt;503&lt;/code&gt; with &lt;code&gt;{"error":"insufficient disk space; try again later"}&lt;/code&gt; and a &lt;code&gt;Retry-After: 10&lt;/code&gt; header, so the agent knows to wait instead of guessing. (When this post first went up, they were a plain &lt;code&gt;500&lt;/code&gt;. That's fixed now.)&lt;/p&gt;

&lt;p&gt;Until then, the health endpoint gives the agent a direct answer. It reports both folders, and its overall status turns to &lt;code&gt;degraded&lt;/code&gt; when either one is over 90%:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$AH_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$AH_URL&lt;/span&gt;&lt;span class="s2"&gt;/v1/system/health/detailed?fresh=true"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  | jq &lt;span class="s1"&gt;'{status, disk, docker_disk}'&lt;/span&gt;
&lt;span class="c"&gt;# {&lt;/span&gt;
&lt;span class="c"&gt;#   "status": "degraded",&lt;/span&gt;
&lt;span class="c"&gt;#   "disk": {"total_gb": 75, "free_gb": 41.2, ...},&lt;/span&gt;
&lt;span class="c"&gt;#   "docker_disk": {..., "used_percent": 91.4}&lt;/span&gt;
&lt;span class="c"&gt;# }&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The 80% warning only reaches the server log, so an agent that wants an early heads-up should check &lt;code&gt;used_percent&lt;/code&gt; itself before a big batch of builds and clean up old services first. The health result is cached for 30 seconds, and &lt;code&gt;?fresh=true&lt;/code&gt; cuts that to 10.&lt;/p&gt;

&lt;h2&gt;
  
  
  A speed limit for each agent
&lt;/h2&gt;

&lt;p&gt;Each agent gets its own account on the server, which &lt;code&gt;ah&lt;/code&gt; calls a tenant, with its own API key. Every tenant has a rate limit of 100 requests per second, with a burst of 200. That means it can send 200 requests in a quick burst, and after that it gets about 100 more each second. The whole server also has a shared limit of 500 per second, with a burst of 1,000, so many agents together can't swamp it either.&lt;/p&gt;

&lt;p&gt;Every authenticated response tells the agent where it stands:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-si&lt;/span&gt; &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$AH_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$AH_URL&lt;/span&gt;&lt;span class="s2"&gt;/v1/services"&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="s1"&gt;'^x-ratelimit'&lt;/span&gt;
&lt;span class="c"&gt;# X-Ratelimit-Limit: 100&lt;/span&gt;
&lt;span class="c"&gt;# X-Ratelimit-Remaining: 187&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Go over and the server answers &lt;code&gt;429&lt;/code&gt; with &lt;code&gt;{"error":"rate limit exceeded"}&lt;/code&gt; and a &lt;code&gt;Retry-After&lt;/code&gt; header saying how many seconds to wait, between 1 and 60.&lt;/p&gt;

&lt;p&gt;There's a second limit for slow requests, like following logs or creating a database. Each tenant can have 4 of those open at once. A fifth gets &lt;code&gt;429&lt;/code&gt; with &lt;code&gt;Retry-After: 5&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;It helps to teach your agent the difference between &lt;code&gt;429&lt;/code&gt; and &lt;code&gt;409&lt;/code&gt;. A &lt;code&gt;429&lt;/code&gt; means "slow down and try the same thing again." A &lt;code&gt;409&lt;/code&gt; means the request itself ran into something, like a full quota ("quota exceeded: maximum 3 databases reached") or an open circuit breaker. Waiting won't fix a &lt;code&gt;409&lt;/code&gt;, so the agent should read the error before it tries again.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I tell my agent
&lt;/h2&gt;

&lt;p&gt;If you write your own instructions for an agent that uses agentic-hosting, this is the short version I'd paste in:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;- Send an Idempotency-Key on any POST you might retry.
  On retry, reuse the same key and the exact same body.
- On 429 or 503, wait Retry-After seconds, then retry.
- On 409, read the error before trying again.
- If circuit_open is true: read last_error and
  /deployments, fix the cause, POST /reset once,
  then POST /redeploy (or start a new build).
- Before a big batch of builds, check
  /v1/system/health/detailed for disk used_percent.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These limits don't make the agent any smarter. They keep one bad loop from taking down the server, and they leave a clear error behind for the agent to read.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;An agent that never stops is a great user until it's wrong. agentic-hosting answers with four limits: idempotency keys so retries don't double up, a circuit breaker for crash loops, a disk check that refuses new work at 90%, and a rate limit for each agent. Each one tells the agent what happened, so it can fix the cause instead of trying harder.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/dennisonbertram/agentic-hosting" rel="noopener noreferrer"&gt;Star agentic-hosting on GitHub&lt;/a&gt; so more people find it.&lt;/li&gt;
&lt;li&gt;Give it to your agent: &lt;a href="https://raw.githubusercontent.com/dennisonbertram/agentic-hosting/main/llms.txt" rel="noopener noreferrer"&gt;llms.txt&lt;/a&gt; to sign up and set up, or the &lt;a href="https://raw.githubusercontent.com/dennisonbertram/agentic-hosting/main/skills/agentic-hosting/SKILL.md" rel="noopener noreferrer"&gt;agent skill&lt;/a&gt; for daily use.&lt;/li&gt;
&lt;li&gt;Try the &lt;a href="https://dennisonbertram.github.io/agentic-hosting/docs/install/" rel="noopener noreferrer"&gt;quickstart&lt;/a&gt;, or start at the &lt;a href="https://dennisonbertram.github.io/agentic-hosting/" rel="noopener noreferrer"&gt;docs site&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/dennisonbertram/agentic-hosting/issues" rel="noopener noreferrer"&gt;Open an issue&lt;/a&gt; if your agent finds a way to get stuck that these limits don't catch. I want to hear about it.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>devops</category>
      <category>selfhosted</category>
      <category>api</category>
    </item>
    <item>
      <title>My AI Agent Deploys Code to My Server. Here's What Keeps It in a Box.</title>
      <dc:creator>Dennison Bertram</dc:creator>
      <pubDate>Sat, 17 Oct 2026 14:21:16 +0000</pubDate>
      <link>https://dev.to/dennisonbertram/my-ai-agent-deploys-code-to-my-server-heres-what-keeps-it-in-a-box-1iog</link>
      <guid>https://dev.to/dennisonbertram/my-ai-agent-deploys-code-to-my-server-heres-what-keeps-it-in-a-box-1iog</guid>
      <description>&lt;p&gt;My coding agent deploys apps to a server I own. A lot of the time it's shipping code it wrote ten minutes earlier, and I haven't read every line. Usually that's fine. The question I kept coming back to was what happens on the day it isn't fine: a bad dependency, a confused agent, or an app that starts poking at the machine it runs on.&lt;/p&gt;

&lt;p&gt;My answer is a sandbox around every app the agent ships. I built it into &lt;a href="https://github.com/dennisonbertram/agentic-hosting" rel="noopener noreferrer"&gt;agentic-hosting&lt;/a&gt;, an open-source tool that turns a Linux server into a place AI agents can deploy apps and databases on their own. This post explains in plain terms how that sandbox works, how to set it up on your server, and how to check for yourself that an app is really inside it.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;⭐ If this is useful, a star on &lt;a href="https://github.com/dennisonbertram/agentic-hosting" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; helps other people find it.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;🤖 &lt;strong&gt;Give this to your agent:&lt;/strong&gt; point it at &lt;a href="https://raw.githubusercontent.com/dennisonbertram/agentic-hosting/main/llms.txt" rel="noopener noreferrer"&gt;https://raw.githubusercontent.com/dennisonbertram/agentic-hosting/main/llms.txt&lt;/a&gt; and it can sign up and set things up from that one link.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why a normal container isn't enough
&lt;/h2&gt;

&lt;p&gt;Most apps today ship as containers. A container feels like its own small computer, with its own files and processes. It isn't one. Every container on a machine shares the same Linux kernel with the host and with every other container. The kernel is the core program that controls files, memory, the network, and everything else.&lt;/p&gt;

&lt;p&gt;Programs ask the kernel for things through system calls, or syscalls. Opening a file is a syscall. So is sending network data or starting a process. In a normal container, those calls go straight to the host's real kernel. Linux has hundreds of them, and every so often someone finds a bug in one that lets a program break out of its container and reach the host. That's called a container escape.&lt;/p&gt;

&lt;p&gt;When I write and deploy the code myself, I can live with that risk. When an agent writes and deploys code all day without me watching, I want another wall.&lt;/p&gt;

&lt;h2&gt;
  
  
  What gVisor does
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://gvisor.dev/docs/" rel="noopener noreferrer"&gt;gVisor&lt;/a&gt; is an open-source project from Google that adds that wall. It gives each container its own kernel, written in Go, that runs as an ordinary program on the host. gVisor calls this kernel the Sentry.&lt;/p&gt;

&lt;p&gt;When the app makes a syscall, the Sentry handles it itself instead of passing it to the host kernel. When the app needs files, a separate helper process called the Gofer fetches them. The Sentry itself runs with tight limits on what it can ask the real kernel for. So if the app finds a bug, it's a bug in gVisor's kernel, and getting from there to the host takes a second break-in.&lt;/p&gt;

&lt;p&gt;gVisor plugs into Docker as a runtime, which is the piece that actually starts containers. Docker's default runtime is called &lt;code&gt;runc&lt;/code&gt;. gVisor's is called &lt;code&gt;runsc&lt;/code&gt;. You use the same Docker commands with a different engine underneath.&lt;/p&gt;

&lt;p&gt;There are costs, and gVisor's docs are upfront about them. Each syscall takes a longer path, so apps that make a lot of them, like heavy disk or network work, run slower. gVisor also doesn't implement every Linux syscall, so once in a while an app won't run unchanged (see the &lt;a href="https://gvisor.dev/docs/user_guide/compatibility/" rel="noopener noreferrer"&gt;compatibility notes&lt;/a&gt;). For the web apps and APIs I have agents deploy, I'll take that trade.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setting up gVisor for agentic-hosting
&lt;/h2&gt;

&lt;p&gt;agentic-hosting is a single Go program called &lt;code&gt;ah&lt;/code&gt; that runs on your server and takes deploy requests from agents over an HTTP API. The &lt;a href="https://dennisonbertram.github.io/agentic-hosting/docs/install/" rel="noopener noreferrer"&gt;quickstart&lt;/a&gt; covers the full install. Here's the gVisor part, on Ubuntu.&lt;/p&gt;

&lt;p&gt;Install &lt;code&gt;runsc&lt;/code&gt; from gVisor's package repository:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;KEYRING&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/usr/share/keyrings/gvisor-archive-keyring.gpg
&lt;span class="nv"&gt;ARCH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;dpkg &lt;span class="nt"&gt;--print-architecture&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nv"&gt;REPO&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;https://storage.googleapis.com/gvisor/releases

curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://gvisor.dev/archive.key &lt;span class="se"&gt;\&lt;/span&gt;
  | &lt;span class="nb"&gt;sudo &lt;/span&gt;gpg &lt;span class="nt"&gt;--dearmor&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$KEYRING&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"deb [arch=&lt;/span&gt;&lt;span class="nv"&gt;$ARCH&lt;/span&gt;&lt;span class="s2"&gt; signed-by=&lt;/span&gt;&lt;span class="nv"&gt;$KEYRING&lt;/span&gt;&lt;span class="s2"&gt;] &lt;/span&gt;&lt;span class="nv"&gt;$REPO&lt;/span&gt;&lt;span class="s2"&gt; release main"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  | &lt;span class="nb"&gt;sudo tee&lt;/span&gt; /etc/apt/sources.list.d/gvisor.list
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt-get update &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;sudo &lt;/span&gt;apt-get &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; runsc
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then register it with Docker under the name &lt;code&gt;runsc&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo tee&lt;/span&gt; /etc/docker/daemon.json &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;JSON&lt;/span&gt;&lt;span class="sh"&gt;'
{
  "runtimes": {
    "runsc": {
      "path": "/usr/bin/runsc"
    }
  },
  "default-address-pools": [{"base":"172.20.0.0/14","size":24}]
}
&lt;/span&gt;&lt;span class="no"&gt;JSON
&lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl restart docker
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;runtimes&lt;/code&gt; block is the part that matters. &lt;code&gt;ah&lt;/code&gt; asks Docker for a runtime named exactly &lt;code&gt;runsc&lt;/code&gt;. The address pool line gives Docker room for a lot of small networks, which &lt;code&gt;ah&lt;/code&gt; needs because it creates a private network for each agent and another one for each app.&lt;/p&gt;

&lt;p&gt;Check that it worked:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker info 2&amp;gt;/dev/null | &lt;span class="nb"&gt;grep &lt;/span&gt;runsc
docker run &lt;span class="nt"&gt;--rm&lt;/span&gt; &lt;span class="nt"&gt;--runtime&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;runsc hello-world
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first command should show &lt;code&gt;runsc&lt;/code&gt; among Docker's runtimes. The second runs a test container inside gVisor.&lt;/p&gt;

&lt;p&gt;That's all the gVisor setup there is. &lt;code&gt;ah&lt;/code&gt; checks for it every time it starts. In production, if Docker doesn't list &lt;code&gt;runsc&lt;/code&gt;, &lt;code&gt;ah&lt;/code&gt; exits with this error:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;FATAL: gVisor runtime (runsc) not found in Docker daemon. gVisor is required for container isolation in production.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;In dev mode (&lt;code&gt;--dev&lt;/code&gt;, for trying it on a laptop) it logs a warning instead, but app containers still won't start, because &lt;code&gt;ah&lt;/code&gt; always asks for &lt;code&gt;runsc&lt;/code&gt;. There's no setting that turns the sandbox off.&lt;/p&gt;

&lt;h2&gt;
  
  
  What happens when an agent deploys
&lt;/h2&gt;

&lt;p&gt;From the agent's side, the sandbox doesn't show up at all. The agent registers, gets an API key, and asks for a service:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$AH_URL&lt;/span&gt;&lt;span class="s2"&gt;/v1/services"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$AH_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{
    "name": "web",
    "image": "your-dockerhub-user/your-app:1.0",
    "port": 3000
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That request takes a name, an image, a port, and optional environment variables. Nothing else. There's no field for the runtime, extra privileges, or more memory, so the agent can't ask for a looser box even if it wanted one.&lt;/p&gt;

&lt;p&gt;Behind that call, &lt;code&gt;ah&lt;/code&gt; starts the container with these settings. They're hardcoded in &lt;code&gt;internal/docker/client.go&lt;/code&gt;, so you can read them yourself.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Runtime &lt;code&gt;runsc&lt;/code&gt;.&lt;/strong&gt; The app runs on gVisor.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;All capabilities dropped&lt;/strong&gt; (&lt;code&gt;CapDrop: ALL&lt;/code&gt;). Linux splits root's powers into pieces called capabilities, like changing file ownership or reconfiguring the network. The app gets none of them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No new privileges&lt;/strong&gt; (&lt;code&gt;no-new-privileges&lt;/code&gt;). A program inside can't gain more power later, for example through &lt;code&gt;sudo&lt;/code&gt; or another setuid program.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Read-only root filesystem&lt;/strong&gt; (&lt;code&gt;ReadonlyRootfs&lt;/code&gt;). The app can't change its own files. It gets small in-memory scratch folders instead: &lt;code&gt;/tmp&lt;/code&gt; (64 MB) and &lt;code&gt;/run&lt;/code&gt;, &lt;code&gt;/var/run&lt;/code&gt;, and &lt;code&gt;/var/tmp&lt;/code&gt; (16 MB each). They're all mounted &lt;code&gt;noexec&lt;/code&gt;, so nothing written there can run as a program.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;256 processes max&lt;/strong&gt; (&lt;code&gt;PidsLimit&lt;/code&gt;). A program that keeps spawning copies of itself hits a wall.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Memory and CPU caps&lt;/strong&gt; from the agent's quota, 2048 MB and 2 CPUs by default. The memory-plus-swap limit equals the memory limit, so the app gets no swap.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A private network.&lt;/strong&gt; Each agent's apps sit on their own Docker network, marked internal, with app-to-app traffic turned off. Web traffic comes in through Traefik, the proxy that routes requests, over a separate small network for each app. None of these networks has a route to the internet.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;ah&lt;/code&gt; also sets the label &lt;code&gt;traefik.enable=false&lt;/code&gt; on every app, so an image can't use its own labels to grab another app's traffic. The Postgres and Redis containers it creates for agents run under &lt;code&gt;runsc&lt;/code&gt; too.&lt;/p&gt;

&lt;p&gt;One practical effect: images that write outside those scratch folders, or that expect to start as root and switch users, will crash here. If an agent's app dies with "Read-only file system" in its logs, that's the reason. The agent can read those logs itself at &lt;code&gt;GET /v1/services/{id}/logs&lt;/code&gt; and fix the app.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to check an app is really sandboxed
&lt;/h2&gt;

&lt;p&gt;I don't like trusting a list of settings in a blog post, including mine. Here's how to check on your own server.&lt;/p&gt;

&lt;p&gt;First, ask &lt;code&gt;ah&lt;/code&gt;. The detailed health endpoint reports whether gVisor is installed and which version:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$AH_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$AH_URL&lt;/span&gt;&lt;span class="s2"&gt;/v1/system/health/detailed"&lt;/span&gt;
&lt;span class="c"&gt;# {&lt;/span&gt;
&lt;span class="c"&gt;#   "status": "ok",&lt;/span&gt;
&lt;span class="c"&gt;#   "docker": {"available": true, ...},&lt;/span&gt;
&lt;span class="c"&gt;#   "gvisor": {"available": true, "version": "..."},&lt;/span&gt;
&lt;span class="c"&gt;#   ...&lt;/span&gt;
&lt;span class="c"&gt;# }&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then look at the container. Get the service's &lt;code&gt;container_id&lt;/code&gt; from the API and ask Docker how it was started (this uses &lt;code&gt;jq&lt;/code&gt; to pull out the field):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;CID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$AH_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$AH_URL&lt;/span&gt;&lt;span class="s2"&gt;/v1/services/&lt;/span&gt;&lt;span class="nv"&gt;$SERVICE_ID&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | jq &lt;span class="nt"&gt;-r&lt;/span&gt; .container_id&lt;span class="si"&gt;)&lt;/span&gt;

docker inspect &lt;span class="nt"&gt;--format&lt;/span&gt; &lt;span class="s1"&gt;'{{json .HostConfig}}'&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$CID&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  | jq &lt;span class="s1"&gt;'{Runtime, ReadonlyRootfs, CapDrop, PidsLimit}'&lt;/span&gt;
&lt;span class="c"&gt;# {&lt;/span&gt;
&lt;span class="c"&gt;#   "Runtime": "runsc",&lt;/span&gt;
&lt;span class="c"&gt;#   "ReadonlyRootfs": true,&lt;/span&gt;
&lt;span class="c"&gt;#   "CapDrop": [&lt;/span&gt;
&lt;span class="c"&gt;#     "ALL"&lt;/span&gt;
&lt;span class="c"&gt;#   ],&lt;/span&gt;
&lt;span class="c"&gt;#   "PidsLimit": 256&lt;/span&gt;
&lt;span class="c"&gt;# }&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On the host, every gVisor sandbox shows up as its own process:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ps &lt;span class="nt"&gt;-eo&lt;/span&gt; pid,args | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="s1"&gt;'[r]unsc-sandbox'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can also look from inside. If the image has &lt;code&gt;dmesg&lt;/code&gt; and &lt;code&gt;touch&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$CID&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; dmesg
&lt;span class="c"&gt;# [    0.000000] Starting gVisor...&lt;/span&gt;

docker &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$CID&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nb"&gt;touch&lt;/span&gt; /test
&lt;span class="c"&gt;# ... Read-only file system&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Under gVisor, &lt;code&gt;dmesg&lt;/code&gt; prints gVisor's own made-up boot log instead of the host's kernel log. Some of the lines are jokes, like "Preparing for the zombie uprising...". gVisor's docs point out that this output is easy to fake, so use it as a quick sanity check and trust &lt;code&gt;docker inspect&lt;/code&gt; for the real answer.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the sandbox doesn't cover
&lt;/h2&gt;

&lt;p&gt;gVisor protects the server from the apps. It doesn't solve everything.&lt;/p&gt;

&lt;p&gt;Builds are a separate step. When an agent deploys from git, Nixpacks (the tool that turns source code into a container image) builds it with Docker's regular builder, not inside gVisor. The app that comes out of the build runs sandboxed. &lt;code&gt;ah&lt;/code&gt; limits where the source can come from: HTTPS URLs on github.com, gitlab.com, bitbucket.org, sr.ht, or codeberg.org.&lt;/p&gt;

&lt;p&gt;The API key is the other gap. Anyone holding an agent's key can do anything that agent can, including deploying, deleting, and reading its secrets. They can't reach other agents' apps or register new agents. I give each agent its own key with &lt;code&gt;POST /v1/auth/keys&lt;/code&gt; and revoke it when I'm done.&lt;/p&gt;

&lt;p&gt;And gVisor is one more wall, not a guarantee. Keep the host patched.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;Every app an agent deploys through agentic-hosting runs inside gVisor, with no extra privileges, a read-only filesystem, capped resources, and a network that only lets web traffic in. The agent can't turn any of that off, and you can check the main settings yourself with one &lt;code&gt;docker inspect&lt;/code&gt; command.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/dennisonbertram/agentic-hosting" rel="noopener noreferrer"&gt;Star agentic-hosting on GitHub&lt;/a&gt; so more people find it.&lt;/li&gt;
&lt;li&gt;Give it to your agent: &lt;a href="https://raw.githubusercontent.com/dennisonbertram/agentic-hosting/main/llms.txt" rel="noopener noreferrer"&gt;llms.txt&lt;/a&gt; to sign up and set up, or the &lt;a href="https://raw.githubusercontent.com/dennisonbertram/agentic-hosting/main/skills/agentic-hosting/SKILL.md" rel="noopener noreferrer"&gt;agent skill&lt;/a&gt; for daily use.&lt;/li&gt;
&lt;li&gt;Try the &lt;a href="https://dennisonbertram.github.io/agentic-hosting/docs/install/" rel="noopener noreferrer"&gt;quickstart&lt;/a&gt;, read the &lt;a href="https://dennisonbertram.github.io/agentic-hosting/docs/security/" rel="noopener noreferrer"&gt;security model&lt;/a&gt;, or start at the &lt;a href="https://dennisonbertram.github.io/agentic-hosting/" rel="noopener noreferrer"&gt;docs site&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/dennisonbertram/agentic-hosting/issues" rel="noopener noreferrer"&gt;Open an issue&lt;/a&gt; if you find a way out of the box. I want to hear about it.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>security</category>
      <category>docker</category>
      <category>selfhosted</category>
    </item>
  </channel>
</rss>
