Running the Agent in a Container
By the end of this page you will have an agent served by OmniServe in a container that runs as a non-root user on a read-only filesystem, reads its model key from the environment, keeps everything it must remember on one volume, and reports itself healthy only when it can take work. Replace the container, and its runs, sessions, traces and scheduled tasks are still there.Every output on this page is what the commands printed when they were run,
with an
omnicoreagent 0.5.0 release candidate installed from its wheel in the
image (the commented lines in the Dockerfile below).Build the image
An agent file,agent.py, with its memory in SQL so its runs survive:
Dockerfile:
postgresis the SQL extra: SQLAlchemy, used for SQLite too.- The image holds no secret. The model key arrives at run time.
- The health check asks
/ready, which istrueonly once the agent is initialized and its MCP servers are connected (/healthonly says the process is up; neither answers before startup has finished, which can take up to a minute on a busy machine, so raise--start-periodon a slow host). The slim image has nocurl, so it uses Python.
Run it
-e LLM_API_KEY with no =value passes the variable through from your
shell, so the key is not in your shell history or on the docker run
command line. It is still in the container’s environment: docker inspect
shows it under Config.Env to anyone who can reach the Docker daemon. In
production, inject it the same way from your platform’s
secret store. LLM_API_KEY is the only model key variable the runtime reads.
/tmp
and the /data volume are writable), no Linux capabilities, no privilege
escalation, memory and process limits. The agent needed nothing else.
Ask it something as on the OmniServe page:
What is on the volume
After a run and a scheduled task:
Without the volume, all of it is lost with the container. Without
OMNICOREAGENT_WORKSPACE_DIR the workspace is ./workspace, relative to the
working directory (/app here, which is read-only).
Replace the container, keep the state
A task created before, then the container removed and a new one started on the same volume:task_id, schedule.expression and
schedule_state.next_due_at:
GET /runs/{run_id} returned
its record, and GET /runs/{run_id}/trajectory its trace (traces_missing
0). A run that was paused for an approval can be decided and resumed on the
new container.
Scale beyond one container
SQLite on a volume is for one container. For several, each one a single OmniServe process, share a database server instead of the volume:MemoryRouter("sql") with a PostgreSQL DATABASE_URL, or "redis" with
REDIS_URL, or "mongodb" with MONGODB_URI; and a task store to match.
Each run belongs to one process at a time; a run whose process dies is picked
up by another once its lease expires. Traces are written to each container’s
workspace, so keep the workspace on shared storage
(OMNICOREAGENT_WORKSPACE_BACKEND=s3 or r2) or export traces
(Observability).
Stores and scale covers this.
Sandboxes from inside a container
If the agent runs commands (theexecute tool, code mode), give it a
sandbox. From inside a container:
A hosted provider (E2B, Modal, Daytona, Vercel, or your own http
service) needs only its extra, its key in the environment, and outbound
network. This is the simple choice.
The Docker sandbox starts containers through a Docker engine, found as
the Docker SDK finds one (DOCKER_HOST, else /var/run/docker.sock). A
container has neither. With omnicoreagent[docker] in the image, the
engine’s ping from inside it:
workspace_mount, if you configure one, is a path on the engine’s host,
not in the agent’s container.
Options
omniserve generate-dockerfile --file agent.py writes a starting
Dockerfile (OmniServe);
it installs only [serve], checks /health, and keeps the workspace in
/tmp/workspace, so add a volume and the extras you use.
A script instead of a server works the same way: CMD ["python", "my_job.py"],
with the same variables and volume.
When things go wrong
Application startup failed: LLM_API_KEY not found
Application startup failed: LLM_API_KEY not found
The container was started without the key (or Export
-e LLM_API_KEY with the
variable unset in your shell). It exits:LLM_API_KEY in the shell that runs docker run, or set it in
your platform’s secrets.Runs end with provider_error: the provider rejected the API key
Runs end with provider_error: the provider rejected the API key
The container is healthy, but the key is wrong:Check which provider the key belongs to and
model_config["provider"].Everything is gone after a restart
Everything is gone after a restart
State was in memory or in the container’s filesystem. Mount a volume
(or use a database server), set
DATABASE_URL with
MemoryRouter("sql"), a durable task store, and
OMNICOREAGENT_WORKSPACE_DIR on the volume. If DATABASE_URL is unset,
the agent does not start: MemoryRouter("sql") raises ValueError: MemoryRouter("sql") needs DATABASE_URL.Read-only file system
Read-only file system
With
--read-only, only /tmp and mounted volumes are writable. Point
the workspace and SQLite files at the volume, as above.Docker sandbox: FileNotFoundError or PermissionError
Docker sandbox: FileNotFoundError or PermissionError
FileNotFoundError(2, ...): no Docker engine is reachable from the
container. PermissionError(13, ...): the socket is mounted, but the
container’s user is not in its group (--group-add). Read the warning
above before doing either; a hosted provider avoids both.Next
OmniServe
Every route, the configuration, the errors.
Stores and scale
Choosing stores, and several containers sharing them.
Durable runs
What a run’s record holds, and recovery after a crash.
Background agents
Scheduled work, retries and leases.
Sandbox providers
Docker, E2B, Modal, Daytona, Vercel, your own.