How Kamal Fits My Coding-Agent Workflow
I run a bunch of small products, and most of the work now happens through coding agents. That changes what I want from deployment.
Clicking around in a deployment platform is the wrong interface for that. I want deployment to be visible in the repo, scriptable from a terminal, and predictable when several agents are working at once.
For my current setup, Kamal is enough.
The setup is one physical server
My current pattern is simple: one Hetzner server, Docker containers, Kamal configs in each repo, and PostgreSQL on the same box.
The setup is intentionally small: one solo operator running small products that need to stay cheap and understandable.
The repo evidence is plain:
- MyOG.social deploys
web,backend, andapiroles to the same host. - TheBlue.social deploys
web,backend,feed, and worker roles to the same host. - TheBlue’s
AGENTS.mdsays it migrated from Render to Hetzner with Kamal 2 on April 8, 2026. - TheBlue shares that server and the PostgreSQL accessory with MyOG.social and AltCaption.com.
- MyOG’s Kamal config defines the shared PostgreSQL accessory and the Vector logging accessory.
I don’t set up a new database server for every tiny product. MyOG was the first project in this setup, so it owns the shared server pieces. The newer projects follow the pattern instead of inventing their own.
MyOG was the first one
MyOG.social has the older Kamal setup in my repos. Its history shows the move in December 2025:
2025-12-20 Add Kamal 2 deployment configuration for Hetzner with PostgreSQL, Vector logging, SSL, and pre-rendering support
2025-12-19 Remove legacy render.yaml now using Kamal on Hetzner
The config is what I like about Kamal. The deployment state is a file:
service: myog
servers:
web:
hosts:
- <server-ip>: frontend
backend:
hosts:
- <server-ip>: backend
proxy:
ssl: true
host: backend.myog.social
app_port: 3001
api:
hosts:
- <server-ip>: api
proxy:
ssl: true
host: api.myog.social
app_port: 3002
accessories:
postgres:
image: postgres:16-alpine
host: <server-ip>
vector:
image: timberio/vector:latest-alpine
host: <server-ip>
I trimmed that snippet, but the important parts are there: roles, host, proxy, app ports, accessories.
Kamal’s own docs describe the same model: roles live under servers, accessories are managed separately from the app, and the proxy routes traffic to the new container after the health check passes.
That maps well to how I want agents to work. If a deploy fails, the agent can read config/deploy.yml, compare it with the Dockerfile and runtime scripts, inspect logs, and make a repo change.
No special dashboard integration needed.
TheBlue Migrated Later
TheBlue.social moved later, after the MyOG setup had already proved itself.
Its AGENTS.md records the date directly:
Migrated from Render to Hetzner (Kamal 2) on 2026-04-08.
Shares the Hetzner server and Postgres accessory
with MyOG.social and AltCaption.com.
TheBlue is also a good example of the pattern growing without turning into a platform.
It started as a web/backend app, then picked up more moving parts: feed service, background workers, and a lot of pre-rendered SEO pages. The Kamal setup split with it:
config/deploy.yml # web
config/deploy.backend.yml # backend + workers
config/deploy.feed.yml # custom feed service
The web deploy has Caddy serving the built frontend and host-mounted pre-rendered HTML releases. Backend deployment covers the API and workers. Feed has its own image and service.
Still the same idea: Docker images, Kamal configs, one server, explicit roles.
The deploy script detects changed areas and deploys the right targets:
scripts/push-and-deploy.sh
scripts/push-and-deploy.sh --force
kamal deploy --roles=web
kamal deploy -c config/deploy.backend.yml
kamal deploy -c config/deploy.feed.yml
It also handles a practical rule I care about: push to GitHub after deploy succeeds. If production deploy fails, origin/main should not advance as if everything shipped.
That rule matters more once agents are allowed to deploy. If production deploy fails, I want the repo state to say that clearly too.
Why this works with coding agents
Coding agents are good at text, diffs, commands, and logs. Kamal gives them exactly that.
A deployment problem usually becomes one of these:
- a missing secret name in
.kamal/secretsorconfig/deploy.yml - a bad health check path
- a Docker build issue
- a runtime script that starts the wrong process
- a frontend build-time env var that was treated like a runtime env var
- a role split that needs a different deploy config
An agent can inspect those repo problems.
My project AGENTS.md files tell agents how to deploy, which package manager to use, how long web builds usually take, what not to print, and when to run the deploy in tmux with a callback.
That last part is important because deploys are long-running enough to be annoying inside a chat turn. The TheBlue instructions say web deploys have taken about 6.7 minutes, and that Vite can sit quietly at rendering chunks... for several minutes. The point of writing that down is to stop an agent from killing a deploy just because nothing printed for a while.
I want the agent to know that before it touches the system.
The deployment lock handles the shared server
One server is simple, but RAM is finite.
If I have three coding agents working on three products, I do not want all three running Kamal deploys against the same server at the same time.
Agent Control handles that with a shared deploy lock:
deploy:hetzner-main
The rule is: acquire the lock before starting a Hetzner/Kamal deploy, keep it through verification, then release it. If another session already has it, the new deploy waits.
The actual workflow is still the normal project deploy. The lock wraps it.
Each repo still owns its deploy command. Agent Control serializes access to the shared server.
So the flow becomes:
agent finishes change
agent runs checks
agent commits
agent asks for deploy lock
Agent Control queues it if another deploy is running
agent deploys in tmux when lock is acquired
agent verifies production
agent releases lock
agent reports back
This is the part that makes one server work with agent-driven development. The server stays simple, but the deployment jobs are serialized.
Where I Draw the Line
One server has limits.
If the server is down, multiple products are affected. More capacity means moving pieces around. A product that grows a lot may need to leave the shared setup.
That is fine for where these products are now.
Most small products do not need a platform on day one. They need backups, logs, health checks, repeatable deploys, and a setup the owner understands. My setup has those pieces without making me operate a deployment platform.
Kamal is not magic. It still uses Docker, SSH, a registry, env files, health checks, and a proxy. The useful part is that those pieces are explicit and close to the code.
That fits my work better than a sprawling platform.
What I Keep Reusing
The pattern I keep copying into newer projects:
- one Kamal config per deployable boundary when the app grows
web,backend,feed, or worker roles instead of one giant container.kamal/secretsreading from.env.kamal, without committing values- app-specific
AGENTS.mddeploy notes - push/deploy scripts that detect changed services
- tmux for deploys that take minutes
- Agent Control callbacks after long-running commands
deploy:hetzner-mainfor shared-server deploy serialization
For my current small-products life, this is enough infrastructure.
I can tell a coding agent to fix something, run the checks, deploy it, verify it, and report back. The deploy path is plain text the agent can read and reason about.