Docker runtime environment for a multi-agent system based on the webinar
by Petr Ludwig: "AI Agents as the Future of Work". A single isolated container
installs the following tools. Codex and Claude Code are required; installers for
the other webinar tools are best-effort and make health reports any failure:
| Tool | Role | Command |
|---|---|---|
| Codex (OpenAI) | meta-agent, manages other agents | codex |
| Claude Code (Anthropic) | development / agent | claude |
| Google Antigravity | agent (API-only on servers) | agy |
| Hermes Agent (Nous Research) | agent + gateway | hermes |
| OpenClaw | full-stack agent solution | openclaw |
| Agent2Telegram | agent ↔ Telegram bridge | agent2telegram |
| AgentsMonitor | monitoring + automatic recovery | agentsmon |
| Whisper (faster-whisper) | voice control (Voice2Text) | voice2text |
The webinar recommends running agents directly on a server with all safety guards
disabled (--dangerously-skip-permissions, --dangerously-bypass-approvals-and-sandbox,
sudo NOPASSWD on reboot/dd, and a master agent with full server control
connected to Telegram). This is convenient, but dangerous on a bare server —
a misbehaving agent or a successful prompt-injection attack gains full control over
the machine.
This project's approach: the container is the security boundary.
The container contains damage to the host:
- ✅ runs as unprivileged user
agent(not root), - ✅
no-new-privileges+ all Linux capabilities dropped (except the minimum the entrypoint needs) → agents hold no capabilities at all;sudois intentionally blocked, - ✅ no Docker socket, no host network, no
privilegedmode, no host bind-mounts → agent cannot touch the host filesystem or control Docker, - ✅ CPU / RAM / PID limits → common runaway-process and fork-bomb damage is bounded; persistent volumes still have no disk quota,
- ✅ no credentials in the image or in git — tools log in interactively and store their credentials in volumes,
- ✅ tool binaries in the image, data in separate volumes (easy to back up and audit).
If an agent is compromised, stop the container, rotate every credential it could read, rebuild the image, and restore only known-good data. A rebuild alone is not an incident-recovery procedure because workspace files, configuration, and cron jobs intentionally survive in volumes.
Be clear-eyed about this — the container boundary contains host damage, not data:
- ❌ Agents can read each other's credentials. Every agent runs as the same
uid 1000 and shares one home directory, so any agent — running with approval
guards disabled — can read
~/.codex,~/.claude, and the SSH keys in~/.ssh. A successful prompt injection does not need to escape the container to hurt you. - ❌ Outbound network access is unrestricted by default. The container can
reach the whole internet, your LAN, and any host service listening on
0.0.0.0(via the Docker gateway, typically172.17.0.1). Nothing stops an agent from sending those credentials somewhere. - ❌ The agents' own actions are not sandboxed. That's the point — they run
with
--dangerously-*. Everything inside the container is fair game to them.
In short: this is a good containment story for a compromised agent wrecking a machine, and no containment at all for a compromised agent stealing credentials. Plan accordingly:
- Give agents accounts with minimal permissions — never your primary account. Assume any credential inside the container may leak.
- Run the container on a dedicated server/VM, not on your workstation and not on a network with sensitive internal services.
- A proxy can reduce accidental outbound access for tools that honor proxy settings, but it is not enforced against a malicious agent. See Network hardening below.
- Keep API keys out of
.envwhere you can. Interactive login stores credentials in a volume, which at least keeps them out of the process environment and out ofdocker inspect. It does not hide them from the agents themselves.
Prerequisites: Docker + Docker Compose on the host.
# 1) Configuration
cd src
cp .env.example .env # no keys needed – tools authenticate interactively
# INSTALL_* build args choose tools (default: all)
# 2) Build the image (downloads and installs all tools)
docker compose build # or: make build
# 3) Start (runs in the background, restart-policy: unless-stopped)
docker compose up -d # or: make up
# 4) Enter the container as agent
make shell # or: docker compose exec -u agent evilagent bash -lAll
makeanddocker composecommands must be run from thesrc/directory. From the project root usemake -C src <target>orcd srconce at the start of a session.
After start the container brings up the shared tmux session main and runs cron.
You configure and start agents manually (see below), then hand them to
AgentsMonitor to keep alive — see Running agents 24/7.
All tools are installed by default. To exclude one, pass a build arg:
docker compose build --build-arg INSTALL_HERMES=falseAvailable flags: INSTALL_CODEX, INSTALL_CLAUDE_CODE, INSTALL_AGENT2TELEGRAM,
INSTALL_HERMES, INSTALL_OPENCLAW, INSTALL_AGENTSMONITOR, INSTALL_ANTIGRAVITY,
INSTALL_WHISPER.
make health starts each CLI with --version; it exits non-zero when an enabled
tool is missing or broken.
Enter the container (make shell) and set up what you need. Each tool handles
its own authentication and saves credentials to a persistent volume, so
you only need to do this once — there are no API keys to put in .env.
codex # interactive login (device flow / API key)
# run as a persistent agent inside tmux:
tmux new -s master 'codex --dangerously-bypass-approvals-and-sandbox'
# detach: Ctrl+B then D reattach: tmux attach -t master
--dangerously-*is safe for your host here – the boundary is the container. It is not safe for your credentials; see the security model.
claude # interactive login (subscription or API key)
claude --dangerously-skip-permissionsagent2telegram connect # follow the prompts; create the bot with @BotFatherhermes config set API_SERVER_KEY "$(openssl rand -hex 24)"
hermes config set API_SERVER_ENABLED true
nohup hermes gateway run --replace > ~/hermes.log 2>&1 &openclaw onboard # copy the code to a browser, paste the URL back
nohup openclaw gateway > ~/openclaw.log 2>&1 &This is what keeps agents alive across restarts — see Running agents 24/7.
agentsmon setup # scan tmux, pick agents to supervise, install cron launcher
agentsmon new # create + launch + register a new agent in one step
agentsmon add # register agents you started yourself
agentsmon status # live status of agents and daemons
agentsmon doctor # sanity-check tools + configagy --dangerously-skip-permissionsSubscription does not work on servers. Log in with a Google Cloud project, or add
GEMINI_API_KEY=...to.envyourself if you want API mode. Google Cloud offers $300 of free credit for 3 months.
The container has no microphone. Copy audio into ~/workspace and transcribe it:
voice2text ~/workspace/recording.m4a small csThen pass the text to an agent. Models are downloaded once to ~/.cache/whisper.
For languages other than English, ElevenLabs Scribe via their API generally
transcribes better than local Whisper models.
Interactive setup is one-time, but a hand-started agent lives only as long as the container process does. A restart, a host reboot, or an OOM kill would otherwise bring the container back healthy and empty — with no agents running at all.
This is AgentsMonitor's job, and there is nothing to configure in .env for
it. It auto-detects agents in tmux, relaunches them properly when they crash
(e.g. claude --resume <id>), and persists itself with cron — which this
container runs. Inside the container:
agentsmon setup # scans tmux, pick which agents to supervise,
# then installs its cron launcherThat writes an @reboot + every-minute crontab, so your agents come back after a
restart and get relaunched if they die. Check on them any time:
make agents # = agentsmon statusTo spin up a new agent already registered for supervision, use agentsmon new.
The shared tmux session main is there for working by hand:
make attach # Ctrl+B then W lists windows, Ctrl+B then D detachesEverything this relies on is on a volume and survives a rebuild: the config
(~/.config/agentsmon), the state and uptime database (~/.local/state/agentsmon),
and the crontab itself (/var/spool/cron/crontabs).
Without AgentsMonitor (
INSTALL_AGENTSMONITOR=false) nothing starts agents for you — but cron is still there, socrontab -ewith an@rebootline of your own works the same way.
The container healthcheck verifies the machinery, not the agents: keepalive,
the tmux session, and cron. A single crashed agent is not a container fault —
AgentsMonitor relaunches it within a minute, and make agents shows the truth.
Cron dying, on the other hand, means nothing would restart anything, so that
turns the container unhealthy:
docker compose ps # STATUS column shows (unhealthy)
docker inspect --format '{{json .State.Health}}' evilagent | jqDocker's restart: unless-stopped does not act on health status, so if you want
the container itself recycled when it goes unhealthy, run a watchdog such as
willfarrell/autoheal alongside it.
AgentsMonitor logs its own supervision to ~/.local/state/agentsmon/agentsmon.log
and serves a status page (agentsmon dashboard, default 127.0.0.1:8765). Note
that this records supervision — restarts and uptime — not the agents' terminal
output, so it is not an audit trail of what an agent actually did.
Everything important lives in named Docker volumes and survives restarts and image rebuilds:
| Volume | Contents |
|---|---|
codex, claude, hermes, openclaw |
per-tool config and credentials |
agent2telegram, agentsmon |
compatibility directories for tool-specific data |
config |
XDG config, including Agent2Telegram, AgentsMonitor, and gcloud |
cache |
downloaded Whisper models, etc. |
localstate |
AgentsMonitor state, uptime DB, its log (~/.local/state) |
crontabs |
cron jobs (agentsmon service) — lives outside $HOME |
ssh |
agent SSH keys |
workspace |
working directory / repositories / agent data |
Tool binaries are in the image, not in volumes. This means docker compose down
does not delete data and an image update does not overwrite credentials.
To explicitly delete all data:
docker compose down -v(irreversible).
Tools and the OS are updated by rebuilding the image; volume data is preserved:
make update
# = backup → docker compose build --pull → up -dA failed backup aborts the update rather than rebuilding on a backup that
didn't happen. Override deliberately with FORCE=1 ./scripts/update.sh.
Manual equivalent:
docker compose build --pull && docker compose up -dAll tool sources are defined in src/scripts/install-tools.sh –
update URLs in one place. Tools install at their latest version; to refresh them
without a full rebuild:
make tools # runs install-tools.sh inside the running container
make toolsis a temporary repair command: it installs into the running container, so its changes are lost when the container is recreated. Prefermake update, which puts the tools in the image.
make backup # -> src/backups/<timestamp>/agent-data.tar.gz
./src/scripts/restore.sh src/backups/2026.../agent-data.tar.gzThe service is briefly stopped so the archive is consistent. The backup includes
config, credentials, agent logs, workspace, and the persistent crontab, then
verifies that the archive is readable and non-empty. Re-downloadable cache data
is excluded. restore.sh accepts both absolute and relative paths and also
restores older archives that did not contain the crontab.
Backups are written to
src/backups/and are gitignored. They contain plaintext credentials: restrict access to them or encrypt them before copying them elsewhere.
| Command | Description |
|---|---|
make up / make down |
start / stop (volumes kept) |
make shell |
shell as agent |
make root-shell |
shell as root (administration) |
make attach |
attach to shared tmux session main |
make logs |
follow container logs |
make agents |
agent status (AgentsMonitor) |
make health |
show tool availability |
make tools |
reinstall / update tools |
make update |
full update |
make backup |
back up agent data |
make lint |
run shellcheck + hadolint (same checks as CI) |
Agents need outbound internet access for model APIs, so the network cannot be fully closed — but leaving it wide open means any agent can exfiltrate every credential it can read, which is all of them. This project keeps networking simple and documents that risk rather than pretending to enforce an allowlist.
You may point compatible tools at an egress proxy through HTTP_PROXY /
HTTPS_PROXY in .env. This helps with normal traffic, but an autonomous agent
can unset those variables or connect directly, so it is not a security boundary.
Actual enforcement requires host/cloud firewall rules or a proxy-only network
topology, which this deliberately simple project does not create for you.
Full network isolation (networks: internal: true) is available as a
commented-out option in src/docker-compose.yml — usable
only for agents that do not need internet access.
make lint # shellcheck + hadolint + compose validationCI (.github/workflows/ci.yml) runs the same linters, builds
the image with the reliable tools, verifies that their commands actually run,
generates an SBOM, scans for fixable high-severity vulnerabilities, and tests
container boot, cron persistence, health behavior, and complete backups. A weekly
and manually triggered job builds and verifies every webinar tool without making
ordinary commits depend on third-party installer availability.
- Tool shows
MISSorFAILafter build. The installers for Agent2Telegram, Hermes, OpenClaw, AgentsMonitor, and Antigravity come from the webinar and their URLs may differ or be temporarily unavailable. Those are best-effort, so the build continues without them. Verify/update the URLs insrc/scripts/install-tools.shand runmake tools. Codex and Claude Code install via npm and are required — if one of them fails, the build fails rather than handing you an image without it. - Container is
unhealthy. The machinery is broken, not an agent. See why:docker inspect --format '{{json .State.Health}}' evilagent | jq. For agents themselves usemake agents— a crashed agent is AgentsMonitor's job and does not turn the container unhealthy. - Agents don't come back after a restart. Check
crontab -linside the container asagent:agentsmon setupshould have installed an@rebootand an every-minute line. Verify cron is running withpgrep -x cron. sudodoesn't work inside the container. Correct — it is intentionally blocked byno-new-privileges. Usemake root-shellfor administration, or add permanent changes tosrc/Dockerfileand rebuild.- Agent can't access a file from the host. The container has no host
bind-mounts (by design, for security). Copy files into the
workspacevolume:docker compose cp myfile evilagent:/home/agent/workspace/. - First start is slow. The entrypoint takes ownership of freshly created volumes. It only does this for volumes whose ownership is actually wrong, so this cost is paid once per volume, not on every start.
.
├── README.md
├── LICENSE
├── .gitignore
├── .gitattributes
├── .github/
│ └── workflows/
│ └── ci.yml # lint + build + smoke test
└── src/
├── Dockerfile # image: Ubuntu + Node + Python + tools
├── docker-compose.yml # service, security, volumes, limits
├── .env.example # configuration template
├── Makefile # shortcuts
└── scripts/
├── install-tools.sh # install/update CLI tools
├── entrypoint.sh # volume init + cron + drop to agent user
├── health.sh # tool inventory (`make health`)
├── container-health.sh # Docker healthcheck probe
├── voice2text.sh # Whisper audio -> text transcription
├── update.sh # backup + rebuild + refresh
├── backup.sh # back up agent data
└── restore.sh # restore from backup
MIT.