Deploy Your Agent

September 2, 2026 · View on GitHub

Get your agent running in production.

Beta: co deploy is in beta. Works well but may change.


Three Options

OptionBest ForIdentity survives a redeploy
co deployQuick deployment, managed hostingno — a fresh container each time
co deploy --to <server>A server you own, that you can ssh intoyes
Self-hostFull control, your own infrastructureyours 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 by co create or co 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:

  1. Validate locally — checks that .co/host.yaml exists, you have an OPENONION_API_KEY, and your entrypoint file calls host()
  2. 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.
  3. Collect env vars — reads your .env file (API keys, database URLs, etc.) to inject into the container
  4. Upload — sends the tarball, project name, entrypoint path, and secrets to the deploy API
  5. Build & run — the backend builds a Docker image from your source, installs requirements.txt, and starts the container
  6. Poll status — CLI checks deployment status every 3 seconds until the container is running or fails
  7. 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 (contains SKILL.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/skills are not auto-deployed. co deploy ships the project plus whatever --skills you name — not your whole local skill library.


After Deployment

Access Your Agent

Your deployed agent exposes these endpoints:

EndpointMethodPurpose
/inputPOSTSend prompt, get response
/wsWebSocketReal-time streaming
/infoGETAgent metadata (name, tools, trust, examples)
/healthGETHealth check
/docsGETInteractive API docs
/admin/logsGETActivity 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
  • summary and examples from host.yaml as 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 projectsynced, 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.