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.
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.
This post walks through how agentic-hosting, 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.
⭐ If this is useful, a star on GitHub helps other people find it.
🤖 Give this to your agent: point it at https://raw.githubusercontent.com/dennisonbertram/agentic-hosting/main/llms.txt and it can sign up and set things up from that one link.
The loop
agentic-hosting is a single program called ah that runs on your server. Agents talk to it over an HTTP API. In the examples below, $AH_URL is your server's address and $AH_KEY is the agent's API key.
The loop your agent runs looks like this:
- Create a service, the server's name for one app.
- Start a build from a git URL.
- Stream the build output.
- If it fails, read the end of the log.
- Fix the code, push, and build again.
- When it's running, check the URL.
Step 1: make a place for the app
A service needs an image when you create it. That's the packaged app Docker runs. For a git deploy there's no image yet, so send the placeholder "pending", which is what agentic-hosting's own command-line tool sends:
curl -s -X POST "$AH_URL/v1/services" \
-H "Authorization: Bearer $AH_KEY" \
-H "Content-Type: application/json" \
-d '{"name":"web","image":"pending","port":3000}' \
| jq '{id, status, url}'
Save the id as $SVC. The response already has the address the app will live at. If your server was started with a base domain like apps.example.com, that's https://web.apps.example.com. Without one, it's http://<service-id>.localhost, which only works on the server itself.
One thing that looks scary and isn't: creating a service always kicks off a deploy, and a deploy of an image called pending fails. So the service will show failed until the first build lands. The build replaces the image, so your agent can ignore that.
port 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.
Step 2: hand it the git link
Now the build. agentic-hosting uses Nixpacks, an open-source tool that looks at a repo, works out what kind of app it is (a package.json means Node, a requirements.txt 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.
BODY=$(jq -n --arg sha "$(git rev-parse HEAD)" '{
source_type: "git",
source_url: "https://github.com/you/app",
source_ref: $sha }')
curl -s -X POST "$AH_URL/v1/services/$SVC/builds" \
-H "Authorization: Bearer $AH_KEY" \
-H "Content-Type: application/json" \
-d "$BODY"
# {"build_id":"9f2c...",
# "image":"127.0.0.1:5000/ah/...",
# "status":"pending"}
The server answers 202 right away and does the work in the background. Save build_id as $BUILD.
A few rules about the URL:
- HTTPS only, and no username or token in it. In practice that means public repos for now.
- Known hosts only: 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.
-
source_refcan be a branch, a tag, or a commit SHA. It defaults tomain.
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.
Step 3: watch the build
This is the step most hosting tools keep on a web page. Here it's a plain-text stream:
curl -sN -H "Authorization: Bearer $AH_KEY" \
"$AH_URL/v1/services/$SVC/builds/$BUILD/logs?follow=true"
-N 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 ah itself start with [ah]. Everything else is the raw output from git, Nixpacks, and your app's install and build commands.
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.
Step 4: read the failure
Here's a failure I see a lot from agents. It added a package to package.json and never updated package-lock.json. On its laptop that's fine. Nixpacks runs npm ci, which refuses to install when the two files disagree. The end of the log looks something like this (trimmed):
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 (...)
The agent doesn't need to watch the stream to find this. Once the build is done, the same endpoint without follow returns the whole log, and the build's status says how it ended:
curl -s -H "Authorization: Bearer $AH_KEY" \
"$AH_URL/v1/services/$SVC/builds/$BUILD" | jq -r .status
# failed
curl -s -H "Authorization: Bearer $AH_KEY" \
"$AH_URL/v1/services/$SVC/builds/$BUILD/logs" \
| tail -n 40
The tail 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.
A status of succeeded means the image built and was deployed. failed covers everything else, including a build your agent cancelled.
Step 5: fix it and build again
The agent does what you'd do. It runs npm install, 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.
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.)
Step 6: check the URL
When the stream ends with [ah] Deploy succeeded, look at the service:
curl -s -H "Authorization: Bearer $AH_KEY" \
"$AH_URL/v1/services/$SVC" \
| jq '{status, url, last_error}'
# {"status": "running",
# "url": "https://web.apps.example.com", ...}
Then have the agent actually request that URL. running means the container started. It doesn't mean the app answers.
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 PORT environment variable if the service has one. It doesn't set PORT inside the container for you. So if the app listens on 3000 and nobody said so, the container runs fine and every request fails.
The fix doesn't need a new build. Set PORT (most frameworks read it, and the server uses it for routing too), then redeploy, which restarts the same image with the new settings:
curl -s -X POST "$AH_URL/v1/services/$SVC/env" \
-H "Authorization: Bearer $AH_KEY" \
-H "Content-Type: application/json" \
-d '{"PORT":"3000"}'
curl -s -X POST -H "Authorization: Bearer $AH_KEY" \
"$AH_URL/v1/services/$SVC/redeploy"
Redeploy doesn't pull new code. For a code change, start a new build.
Things that trip agents up
- One build at a time per agent. 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.
- Builds have time limits. 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.
-
Cancelling.
DELETE /v1/services/$SVC/builds/$BUILDanswers{"status":"cancelled"}, but the build itself is then markedfailed, with "Build cancelled by user" at the end of its log. Once a build has moved on to deploying, cancelling returns409. -
A full disk says so. If the server's disk is over 90% full, starting a build returns
503with aRetry-Afterheader. I wrote about that and the other limits in my last post.
What I tell my agent
If you write your own instructions for an agent that deploys this way, here's the short version:
- 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.
Wrapping up
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.
- Star agentic-hosting on GitHub so more people find it.
- Give it to your agent: llms.txt to sign up and set up, or the agent skill for daily use.
- Try the quickstart, or start at the docs site.
- Open an issue if your agent hits a build failure it can't read its way out of. I want to see the log.
Top comments (0)