Deploying your agent

An OpenRoutines agent deploys as a plain Docker container. Anything that runs a container runs your agent — a VPS, Fly, Render, Kamal, your homelab. There is nothing else to provision: no database, no queue, no secrets platform. At boot, the supervisor automatically chooses the strongest run sandbox the host supports.

The one prerequisite is a git repository the agent can push to — GitHub, GitLab, Gitea, even a bare repo on a VPS — since that’s where knowledge durably lives. (Local development needs no repository, and openroutines check verifies one is configured before you deploy.)

Deploying

First, set repo in openroutines.yml to the published repository:

repo: https://github.com/acme/my-agent

Then add a deploy key scoped to the repository to give the agent write access to push its knowledge. Generate it outside the agent repo, since a private key must never enter git or the image:

ssh-keygen -t ed25519 -f ~/.keys/my-agent_deploy_key -N "" -C "my-agent deploy key"
gh repo deploy-key add ~/.keys/my-agent_deploy_key.pub --allow-write --title "my-agent"

Then build and run:

docker build -t my-agent .
OPENROUTINES_MASTER_KEY="$(cat master.key)" \
OPENROUTINES_DEPLOY_KEY="$(cat ~/.keys/my-agent_deploy_key)" \
docker run -d --name my-agent --restart unless-stopped --stop-timeout 30 \
  -e OPENROUTINES_MASTER_KEY \
  -e OPENROUTINES_DEPLOY_KEY \
  my-agent

Run sandbox

Every routine runs in its own sandbox, away from the agent repository, key files, and other runs. The supervisor tests the host at boot and uses Bubblewrap when the container runtime permits it, otherwise it falls back to Landlock. There is nothing to select or configure.

On Docker, you can allow the stronger Bubblewrap sandbox with:

docker run ... \
  --security-opt seccomp=unconfined \
  --security-opt apparmor=unconfined \
  --security-opt systempaths=unconfined \
  my-agent

systempaths=unconfined is optional; without it, a run may share the container’s process listing while its files and credentials remain isolated. Some hosts also restrict unprivileged user namespaces. In that case OpenRoutines uses Landlock instead, with no extra configuration.

If the host supports neither sandbox, the supervisor refuses to start. OPENROUTINES_DISABLE_SANDBOX=1 overrides that check, but leaves routines able to reach the repository, key files, and other runs. Use it only when running unconfined is an explicit choice.

The image carries your main branch plus everything the agent runs on, so there’s nothing else to install. Its entrypoint is the supervisor. Two secrets arrive at boot, and neither is ever in the image:

  • The master key decrypts the agent’s credentials, so a routine can receive the ones its frontmatter declares.
  • The deploy key lets the agent push its knowledge. The supervisor fetches the knowledge branch on boot, creating it if it doesn’t exist yet, and pushes what each run recorded.

For each secret, a direct OPENROUTINES_MASTER_KEY / OPENROUTINES_DEPLOY_KEY value wins; otherwise the supervisor reads the path in OPENROUTINES_MASTER_KEY_FILE / OPENROUTINES_DEPLOY_KEY_FILE, then falls back to master.key / deploy.key in the agent root. All three are supported production configurations. The command above passes the values by variable name, keeping the secret text out of the command’s arguments; on a deployment platform, set the same two variables as secrets in the service environment. Neither value reaches a routine or a git subprocess, since the supervisor constructs every child environment from scratch.

If you mount key files somewhere else and point the file variables at them, pick somewhere like /run/secrets, outside what the run sandbox grants — the supervisor refuses to start rather than run with a key file a run could read.

Operational properties

  • Run exactly one instance. The agent is the sole writer to its knowledge branch, so when a platform asks for a replica count, the answer is 1. A lease enforces it, and a second instance waits rather than corrupting knowledge. A clean shutdown hands the lease over immediately; a container killed outright leaves its replacement waiting out a 30-minute TTL.
  • Redeploys are safe. A routine killed mid-run fires again on the next boot, and a scheduled moment that passes while the container is down runs late instead of never.
  • Changes arrive by redeploy. Routines, skills, credentials, and config are all read from the copy of the repo in the image, so a push to main changes nothing in production until you rebuild and redeploy. Knowledge is the only branch a running agent exchanges with origin.
  • One broken routine is one broken routine. A frontmatter typo takes out the routine whose file it’s in, not the agent, and the others keep their schedules. The supervisor records an event naming the file, so the gap shows up in knowledge rather than only in the log.
  • Knowledge survives. Code rolls back with the image. Knowledge lives on its own branch, so it persists like a database, but versioned.
  • No application ingress. The shipped container listens on no ports. What you observe travels outward instead: the supervisor’s log to stderr, and session history to files when you ask for it.

Logs

The supervisor writes to stderr in logfmt, every field a key=value pair, and opencode’s own diagnostics pass through the same stream:

time=2026-07-31T14:52:07.450-04:00 level=INFO msg="attempt starting" routine=check-in run_id=run_abc attempt=attempt_01
timestamp=2026-07-31T18:52:08.104Z level=INFO run=c613738c message="creating instance" directory=/work routine=check-in run_id=run_abc
time=2026-07-31T14:52:31.902-04:00 level=ERROR msg="attempt failed -- will retry" routine=check-in run_id=run_abc detail="exit status 1"

Filter by routine= or by run_id= to follow one run across its retries. opencode’s lines carry the same identity fields, so a run’s diagnostics travel with the supervisor’s records about it.

OPENROUTINES_LOG_LEVEL sets how much of that survives:

  • debug — scheduling, trigger, and knowledge details useful when diagnosing the supervisor. Sandbox selection is always logged at info.
  • info — the default in the container. Lifecycle records, like an attempt starting or a run completing, plus warnings and errors.
  • warn — the default for local commands. Degraded but still running: an unreachable origin, a routine that stopped loading, or a disabled sandbox.
  • error — failed and abandoned runs, and nothing else.

The variable is the only knob, so quieting a live container is an environment change rather than a redeploy. Run output never enters the log at all: openroutines routines run streams it to your terminal on stdout, which is what lets 2>run.log split the two.

Session history

Set OPENROUTINES_SESSION_DIR and each attempt’s sessions are kept, whatever the outcome, as owner-only <UTC timestamp>_<routine>_<run_id>_<session_id>.json files. Leave it unset and nothing is written.

The exports are verbatim, so they can contain credentials, and nothing is pruned for you. Treat the directory as sensitively as what your routines can see.

Continuous deployment

Wire the usual hooks: openroutines check on every push, rebuild and redeploy on merge to main. That redeploy is the only way routine changes reach a running agent. Pushes to the knowledge branch never trigger one, so an agent recording its own work doesn’t redeploy itself.

Updating the framework

Your agent pins the OpenRoutines version it runs against in .openroutines/version, and the deployed container installs exactly that release, so laptop, CI, and production always agree.

openroutines update

This brings the agent up to the version of the binary you’re running, so install the newer binary first. It bumps the pin, rewrites the Dockerfile’s base image tag, and offers any other framework-owned change as a diff you accept or skip. Review, commit, push, and your next deploy runs the new version. Rolling back is git revert.

What’s yours is never touched: routines, skills, knowledge, and credentials belong to the agent, not the framework.