Deploy Your Agent
September 2, 2026 · View on GitHub
Get your agent running in production.
Beta:
co deployis in beta. Works well but may change.
Three Options
| Option | Best For | Identity survives a redeploy |
|---|---|---|
co deploy | Quick deployment, managed hosting | no — a fresh container each time |
co deploy --to <server> | A server you own, that you can ssh into | yes |
| Self-host | Full control, your own infrastructure | yours to arrange |
Pick the middle one when you want the agent to keep its address, its logs and any fix
you made by hand — and to answer on https://<name>.agents.openonion.ai. See
co server.
co deploy (Easiest)
Deploy to ConnectOnion Cloud with one command.
cd my-agent
co auth # If not already authenticated
co deploy
Output:
Deploying to ConnectOnion Cloud...
Project: my-agent
Source: /Users/me/my-agent
Package: 12.3 KB (8 files)
Env: /Users/me/my-agent/.env (3 keys)
Uploading package to https://oo.openonion.ai...
Deployment: a1b2c3d4
Building container on ConnectOnion Cloud...
[1/100] status: deploying
[2/100] status: running
Deployed!
Agent URL: https://my-agent-0x7a9f3b2c.agents.openonion.ai
Dashboard: https://o.openonion.ai/dashboard
Container logs:
Agent started on port 8000
Ready to serve
URL format: {project_name}-{your_address[:10]}.agents.openonion.ai
Re-deploying the same project updates the same URL (like Heroku).
Requirements
.co/host.yaml(created byco createorco init ./)- Authenticated (
co auth) - Entrypoint must call
host()(exports the ASGI app for the container)
How It Works
co deploy
├─ Validate: .co/host.yaml? API key? entrypoint has host()?
├─ Package: git-tracked files when in a repo, otherwise initialized folder
├─ Collect: load env vars from .env
├─ Upload: POST tarball + project_name + secrets + entrypoint to API
├─ Build: backend builds Docker image, installs dependencies
├─ Run: starts container with your env vars injected
├─ Poll: checks status every 3s until running (or error)
└─ Done: returns agent URL + container logs
Step by step:
- Validate locally — checks that
.co/host.yamlexists, you have anOPENONION_API_KEY, and your entrypoint file callshost() - Package source — in git repos, packages tracked files using their current working-tree contents; outside git, packages the initialized folder. Untracked files in a git repo are not deployed. Local-only files such as
.env,.co/keys, caches, logs, docs, and build output are skipped. - Collect env vars — reads your
.envfile (API keys, database URLs, etc.) to inject into the container - Upload — sends the tarball, project name, entrypoint path, and secrets to the deploy API
- Build & run — the backend builds a Docker image from your source, installs
requirements.txt, and starts the container - Poll status — CLI checks deployment status every 3 seconds until the container is running or fails
- Show result — prints the agent URL and fetches the first container logs so you can verify startup
Each deploy creates a new version. The last 5 versions are kept for rollback.
co deploy --template <name> is a shortcut for deploying a template:
it creates .tmp/connectonion-deploy/<name>-agent with the same code path as
co create --template <name> -y, deploys from that generated project, then
deletes .tmp/connectonion-deploy/ after a successful deploy. If deploy fails,
that folder is printed and kept for debugging. Template names are validated by
the same co create template logic, not by a separate deploy allowlist.
Configuration
# .co/host.yaml
name: my-agent # Project name (used in URL) — see the rule below
entrypoint: agent.py # Script to run in container
trust: careful # Trust level for incoming requests
# Agent info — displayed on the frontend landing page
summary: "What your agent does"
examples:
- "Example prompt 1"
- "Example prompt 2"
Naming rule. name becomes a hostname and a Docker image tag, so it must be
1–39 characters of lowercase letters, digits, and hyphens, starting with a
letter or digit. co create and co init ./ write a conforming name for you — a
project created in a folder called My_Project gets name: my-project, and the
adjustment is printed — so this only matters if you edit host.yaml by hand.
co deploy checks it before uploading and suggests the corrected form.
Two projects whose names reduce to the same thing (My_Agent and my-agent)
share one deployment URL, and deploying one replaces the other. Set name
explicitly if you want them kept apart.
Environment Variables
Variables from your .env file are securely passed to your agent container:
# .env
OPENONION_API_KEY=eyJ... # Required for co/ models
CONNECTONION_ADMIN_TOKEN=... # Optional: distinct 256-bit random token for admin monitoring
OPENAI_API_KEY=sk-xxx # Third-party API keys
DATABASE_URL=postgres://...
BROWSER_PROXY=http://user:pass@host:port # Optional browser proxy
BROWSER_PROXY is read by browser tools and routes browser egress through a proxy. See Browser Tools › Proxy.
The agent you deploy (skills + browser)
co create and co init ./ --template co-ai scaffold the same agent the co ai
command runs, wrapped in host(), plus a Dockerfile that ships a real
Chrome + Xvfb browser runtime so browser tools work out of the box.
There is one template. agent.py is about five lines, and you specialise it
with skills in .co/skills/ rather than by starting from a different skeleton:
from connectonion import host
from connectonion.cli.co_ai.agent import create_agent
agent = create_agent(role="coding")
host(agent)
role picks what kind of agent it is. "coding" adds software-engineering
doctrine — read before editing, match the surrounding style, file:line
references, git. An agent that posts to LinkedIn or answers support tickets
wants none of that, so pass role=None. Everything else — how it plans, asks,
reports, and handles actions it cannot take back — is shared, and improves when
the SDK does.
co init ./ --template co-ai
co deploy
No git init/commit in between — co deploy packages the initialized folder
directly when no git repo exists (skipping .env, .co/keys, caches, and
docs), so the freshly scaffolded project deploys as-is.
For a one-command deploy that does not modify the current folder:
co deploy --template co-ai
That creates .tmp/connectonion-deploy/co-ai-agent, deploys it, and cleans it up
after success. Any template supported by co create --template <name> uses the
same flow.
--name sets the project name (and URL) for a template deploy, so different
skill combinations of the same base template can run side by side:
co deploy --template co-ai --name linkedin-agent \
--skills ~/skills/linkedin-login --skills ~/skills/linkedin-post-submit
Skills
The deployed agent loads skills from .co/skills/ via the normal loader.
- Project skills — skills under
.co/skills/deploy with the project. In a git repo they must be tracked by git; outside git they are packaged from the initialized folder:co skills copy <name> # lands in .co/skills/<name>/ co deploy - External skills — to bundle skills that live outside the project, pass
--skills PATH(repeatable). A path that is itself a skill (containsSKILL.md) lands at.co/skills/<dirname>/; a directory of skills has its contents copied into.co/skills/. Your working tree is untouched; on a name clash, later paths win:co deploy --skills /Users/changxing/project/OnCourse/platform/social-media-management-skills co deploy --skills ~/skills/linkedin-login --skills ~/skills/linkedin-post-submit
Your local
~/.claude/skillsare not auto-deployed.co deployships the project plus whatever--skillsyou name — not your whole local skill library.
After Deployment
Access Your Agent
Your deployed agent exposes these endpoints:
| Endpoint | Method | Purpose |
|---|---|---|
/input | POST | Send prompt, get response |
/ws | WebSocket | Real-time streaming |
/info | GET | Agent metadata (name, tools, trust, examples) |
/health | GET | Health check |
/docs | GET | Interactive API docs |
/admin/logs | GET | Activity logs (signed admin or dedicated admin token) |
Frontend (oo-chat)
Users can interact with your agent at:
https://chat.openonion.ai/{your_agent_address}
The landing page shows:
- Agent name, model, trust level
- Tools and skills your agent has
summaryandexamplesfromhost.yamlas suggested prompts- Chat input for conversation
Connect from Code
Python SDK:
from connectonion import connect
agent = connect("0x7a9f3b2c...")
response = agent.input("Hello!")
print(response.text)
HTTP:
curl -X POST https://my-agent-0x7a9f3b2c.agents.openonion.ai/input \
-H "Content-Type: application/json" \
-d '{"prompt": "Hello"}'
co deploy --to (a server you own)
co server new prod # or: co server add prod --ssh you@1.2.3.4
co deploy --to prod
myagent → prod (co@1.2.3.4)
converging server …
syncing code …
installing dependencies …
configuring https …
restarting …
✓ myagent is running on prod
https://prod-abc.agents.openonion.ai — the certificate lands within a minute
logs: co server ssh prod 'journalctl -u myagent -f'
state: /srv/myagent/.co/ — untouched by deploys
What a deploy does
ensure(setup) → sync code → install deps if changed → authenticate live identity → write unit if changed → restart
ensure(setup) is idempotent and a no-op once the server is converged, which is why a
machine registered by hand needs no separate path: its first deploy is the one that
sets it up. A marker at /srv/<agent>/.co/provision.json is read in one ssh call to
decide whether to spend seconds or tens of seconds.
What survives, and what does not
The rsync carries the project tree. Framework-owned state under .co/ is
protected, while project-authored configuration and skills still travel:
.co/keys/ | kept — the agent's address, so its email and every trust relationship hold |
.co/logs/, .co/evals/ | kept — history a dashboard can actually show |
.co/skills/ | synced — skills are what the agent is, not state it accumulated |
| everything else in the project | synced, with --delete, so a deleted file goes away |
The project's root .gitignore is the boundary for its own generated state too.
An ignored path is neither uploaded nor deleted on the server. For example, an agent
that writes a cache under work/ should include:
work/
The live /srv/<agent>/work/ then survives every deploy, including files that exist
only on the server. Non-ignored source still follows the laptop and --delete, so a
source file removed locally is removed remotely. Keep large or irreplaceable state
outside /srv/<agent>/ when possible; otherwise list it in .gitignore before the
first deploy.
The agent runs as itself, not as you
Your project .env names you. co init ./ puts AGENT_ADDRESS, AGENT_EMAIL,
IS_EMAIL_ACTIVE and OPENONION_API_KEY there on purpose, so the project runs on
your account while you are developing it.
On the server those four are wrong, and not inertly so: AGENT_EMAIL overrides the
mailbox the agent derives from its own address, and OPENONION_API_KEY decides whose
credits every model call spends. So the deploy withholds them and substitutes the
agent's own, authenticated from the key the server holds:
myagent → prod (co@1.2.3.4)
…
writing secrets … (5 keys)
account 0xcf1619cb4c… — the agent's own
Everything else in .env — your Gemini key, your database URL — travels unchanged.
Deploy stores those values in the root-owned
/etc/connectonion/<agent>.env (0600) and systemd loads that file. It is
outside /srv/<agent>/, so rsync cannot expose or overwrite it. When Host
startup reports an invite setting such as CO_INVITE_CODE, it names this exact
file; edit it with root privileges, then restart the agent service.
--own-identity mints the key on the machine, so this laptop cannot authenticate as
that agent and no account is written. The deploy says so and the agent has no access
to co/* models until you run co auth in /srv/<agent>:
run co auth in /srv/myagent to give it one; co/* models need it
That is deliberate. An agent with no account fails visibly on its first model call; an agent quietly spending its author's credits does not fail at all — and the spend cannot be separated afterwards, because usage records carry no column naming the machine that made the call.
https and the hostname
A server created by co server new gets a DNS record of its own, and the deploy
installs Caddy in front of the agent. AGENT_PUBLIC_DOMAIN goes into the systemd unit
so the agent announces https://<hostname> to the relay rather than an IP and a port
that is closed — without it clients probe an endpoint that can never answer.
A hand-registered machine gets no https: there is no name of ours to get a certificate for, and inventing one would fail the challenge rather than fail honestly.
One agent per hostname — there is one name and one :443.
Admin
The deploy writes your public address into the agent's .co/admins.txt, every
time. A deployed agent generates its own keypair, and admin actions are gated on it,
so without this nobody could ever administer it — the only account that could grant
admin is the one nobody can sign as. Same idea as ssh-copy-id, one layer up: nothing
secret travels, and revoking is deleting a line.
Self-Host
Deploy to your own VPS or infrastructure using host().
# agent.py
from connectonion import Agent
from connectonion.network import host, create_app
agent = Agent("my-agent", tools=[my_tool])
# Export ASGI app for uvicorn/gunicorn
app = create_app(agent)
if __name__ == "__main__":
host(agent)
Deploy with uvicorn, gunicorn, or any ASGI server:
# Direct
python agent.py
# Uvicorn
uvicorn agent:app --workers 4
# Gunicorn
gunicorn agent:app -w 4 -k uvicorn.workers.UvicornWorker
For full API reference, see host().
When to Use Which
Use co deploy: Fastest path to production, no infrastructure management. Right
until you need the agent to keep its address across deploys.
Use co deploy --to <server>: The agent keeps its identity, its logs and any fix
you made by hand, and it answers on its own https hostname. You can ssh in. Costs a
server ($30/month for the small one, charged yearly) — or nothing, if you point it at a
machine you already have.
Use self-hosting: Full control, custom domains, compliance requirements.