Skip to content

Repository files navigation

title OCaaS - OpenClaw in a Secure Container
description Docker-first OpenClaw appliance with reproducible builds, hardened runtime defaults, and operator-friendly day-2 workflows.
keywords
openclaw
appliance
docker
operator

CI SBOM

OpenClaw is one of those rare tools that earns more real estate in your workflow every week. It started as an AI coding assistant and has quietly become a productivity multiplier, an experiment platform, a learning accelerator, and a research partner. People use it to automate the tedious parts of their work, prototype ideas faster than they can write them down, explore unfamiliar codebases, and wire together workflows that would have taken days to build manually. It is a Swiss-army knife expanding at the pace of ideas, and the skill system means its surface area grows with yours.

Run it on your own hardware and you get a persistent do-anything agent: wire it to model providers, give it skills, and it works autonomously — running code, reading and writing files, calling APIs, browsing the web. Every tool call it makes executes as the user running the process, with full access to your filesystem and network.

On a dedicated host you've set aside for this, that's probably fine. On your daily driver, the agent process runs unsandboxed alongside everything else you care about. That's a real attack surface, and it grows with every skill and provider you add.

This repo puts OpenClaw in a hardened container: read-only root filesystem, UID 1000:1000, all Linux capabilities dropped, secrets injected at runtime and never baked into the image. The container can't write anywhere except the explicitly mounted state directory and a few tmpfs paths. That's the right baseline for something that executes arbitrary tool calls on your behalf, and an absolute minimum if you're running it on a machine you care about.

Beyond the security posture, it adds the operational layer you need to run this reliably over time: reproducible builds from pinned inputs, real OpenClaw CLI validation at every stage, persistent config that survives container replacement, and upgrade and rollback workflows that behave correctly when things go wrong.

What this repo provides

  • Multi-stage Docker build with a pre-fetched, checksummed OpenClaw artifact in the build context
  • Docker Hardened Image runtime stage (dhi.io/node:24)
  • Docker Compose as the canonical runtime
  • External, pinned skill staging outside the Dockerfile
  • Real OpenClaw CLI validation in local builds, CI, startup, and smoke tests
  • Host-mounted persistent runtime state that survives container replacement
  • One-command build, deploy, upgrade, rollback, status, and logs flows
  • GitHub Actions for scheduled builds, candidate promotion, and registry publishing

Get running in three steps

All you need is Docker Desktop and a container image.

1. Run the setup wizard

./bin/ocaas config

The wizard walks you through image selection, state path, gateway token, and port using arrow-key menus, masked input for secrets, and live validation as you go. It writes env/openclaw.env and runs a preflight check before it exits, so you know the environment is sane before you deploy anything.

Prefer scripted or unattended setup? Use the bash path instead:

bash ./scripts/configure.sh --non-interactive --gateway-token '<token>' --image <repo:tag>

2. Start the service

./ops/deploy.sh up

3. Check health

./ops/deploy.sh readiness

Tip

./ops/deploy.sh status gives you a compact runtime summary at any point.

That's the whole flow for getting started. The rest of this document goes deeper, in roughly the order you'll care about it.


Under the hood

Two ideas explain how everything fits together here.

The Dockerfile knows nothing about the network

Most Docker builds fetch things at build time. We don't do that here. Before docker build runs, a separate pipeline step resolves the target OpenClaw version, fetches the npm artifact into build/openclaw-artifact/, checksums it, and records the digest in build metadata. The Dockerfile then COPYs that pre-staged artifact into the multi-stage build.

The practical effect is that every built image has a verifiable provenance trail, and the build never depends on a CDN being up at the moment you run it. It also means you can inspect exactly what went into an image without reading Dockerfile RUN commands. The full pipeline is in docs/build.md.

Your config survives container replacement

This is the question that matters most when you're about to run an upgrade: will my settings still be there? The answer here is yes, by design.

The image carries a seed bundle baked in at build time. Think of it as the factory defaults: a rendered openclaw.json5, workspace baseline files, staged skills, and the build manifest lock, all sitting at /seed/.openclaw inside the image. On first start, the entrypoint copies those files into the host-mounted runtime home, but only where files don't already exist. After that first boot, your mounted state is what OpenClaw reads.

Upgrading to a new image brings a new seed, but it never touches your existing config unless you explicitly ask it to with OPENCLAW_SEED_OVERWRITE=true.

The paths that persist across container replacement:

Host path (under OCAAS_STATE_ROOT) Container path Contents
state/default/openclaw-home/.openclaw /var/lib/openclaw-host/openclaw-home/.openclaw Active config and runtime state
state/default/projects /var/lib/openclaw-host/projects Your project files
extra-skills/default /opt/openclaw/extra-skills (read-only) Any extra skills you've dropped in

Building images

The standard build

make build IMAGE_TAG=local

This resolves build inputs, fetches the OpenClaw artifact, stages skills against the lock file, renders the seed bundle, validates config and skills against a live container, and produces a smoke-tested image. It does a lot, but each step is explicit and logged.

Once you've done a full build and the artifact is already cached, iteration is faster:

make build-local IMAGE_TAG=local

This skips the artifact fetch and reuses the cached build/openclaw-artifact/openclaw.tgz. That means build-local always bakes in whatever version was fetched during the last full make build run — not necessarily the latest. Add SKIP_SMOKE_TEST=1 when you're only tweaking seed or config files and don't need a full health check on every iteration.

Note

If the gateway UI shows an "Update available" banner, it means the cached artifact is older than the current npm release. Run make build IMAGE_TAG=local to pull the latest OpenClaw binary and rebuild the image. The "Update now" button in the UI does nothing in a containerized deployment — the container root is read-only and the binary is baked into the image.

OpenClaw version

The version of OpenClaw baked into each image is controlled by a single file:

config/openclaw.version

This file is committed to the repo and holds the pinned stable version. All build paths read from it — make build, build-image.sh, and the CI resolve step — so every build is reproducible without additional flags.

To use a different version for a single build, pass it explicitly:

make build OPENCLAW_VERSION=latest IMAGE_TAG=local    # npm latest
make build OPENCLAW_VERSION=2026.x.y IMAGE_TAG=local  # specific version

To advance the pin for everyone:

echo '2026.x.y' > config/openclaw.version
git commit config/openclaw.version -m "chore(build): bump openclaw pin to 2026.x.y"

Note

openclaw latest and openclaw stable are not equivalent. The latest npm tag tracks the most recent publish, which may include packaging regressions or breaking changes that have not yet been fully validated. The config/openclaw.version pin points to a version that is known to work with this appliance.


How skills work

The build pipeline has full skill-staging support. Three files in config/manifests/ control what ends up in the image:

  • skills.txt lists which skills to include
  • skills.lock.yaml pins the exact commit SHA and records declared env vars and binaries for each skill
  • agents.txt lists which agent configs to seed

At build time, scripts/stage-skills.py clones the skills repo at the pinned ref and copies each declared skill into build/staged-skills/. CI enforces a full 40-character SHA. Local builds are more lenient but will warn you.

That said, there is no maintained public skills library as part of this project. Vetting skills for anyone other than yourself isn't something this repo takes on — an AI agent with tools is only as trustworthy as the skills you give it, and that judgment belongs with the operator. The hooks are here if you want to build your own skills source and wire it in.

Important

Pinning a SHA makes builds reproducible. It says nothing about whether the skill is safe. Before promoting any candidate build to production, review skills.lock.yaml and spot-check the pinned commit in the skills repo.

Here's what each manifest file actually controls during a build:

File Controls
config/manifests/agents.txt Which agent configs are seeded into the image
config/manifests/skills.txt Which skills are staged and CLI-validated
config/manifests/skills.lock.yaml Exact skill revision, declared env vars, and binaries
config/profiles/default/openclaw.json5.example The tracked base config template for this profile
config/profiles/default/openclaw.json5 Your local secret overrides (gitignored)

Day-to-day operations

All the day-2 workflows run through ./ops/deploy.sh. Every subcommand returns a structured exit code: 0 for success, 1 for a usage error, 2 for a runtime failure, and 3 for partial success. That makes it straightforward to wire into scripts or CI without having to parse output. The full reference is in docs/cli-reference.md.

Upgrading

./ops/deploy.sh upgrade

Before pulling anything, upgrade snapshots the currently running container digest. It then pulls :stable, recreates the container, and polls /healthz until the service comes healthy. If it doesn't become healthy within the deadline, it rolls back automatically. You don't have to babysit it.

The tag it pulls is controlled by OCAAS_UPGRADE_TAG in env/openclaw.env, which defaults to stable. Set it to candidate to track pre-promotion builds instead:

OCAAS_UPGRADE_TAG=candidate

To see what an upgrade would do before committing:

./ops/deploy.sh upgrade --dry-run

Up to three pre-upgrade snapshots are retained by default. Set OCAAS_SNAPSHOT_KEEP if you want more or fewer.

Rolling back

./ops/deploy.sh rollback <tag|digest>

Rollback is idempotent: if the target image is already running, it exits zero and does nothing. Omit the ref entirely and it targets the most recent pre-upgrade snapshot. Like upgrade, it has a --dry-run flag if you want to preview before running.

Snapshots

./ops/deploy.sh snapshot   # capture the current digest as a named snapshot
./ops/deploy.sh snapshots  # list what's retained

Status and logs

./ops/deploy.sh status  # compact summary of what's running
./ops/deploy.sh logs    # tail the live container logs

The audit log

Every upgrade, rollback, and configuration change appends a structured JSON entry to $OCAAS_STATE_ROOT/audit.log. Each entry captures the operation type, a timestamp, the image refs before and after the change, and the exit code. Docker daemon logs disappear when you restart Docker; this doesn't.

Concurrency protection

Mutating operations acquire a PID-file lock before touching anything. If you accidentally trigger two upgrades at once, the second one is rejected cleanly with exit code 2 rather than both running in parallel and corrupting state. A stale lock from a crashed process is detected automatically and cleaned up on the next run.


Security

Running OpenClaw directly on your host means the process inherits your user permissions, your filesystem access, and your network. For local development that's fine. For anything production-adjacent, you want meaningful distance between the process and the host.

Here's what this setup actually enforces:

  • The container process runs as UID 1000:1000, not root
  • The root filesystem is mounted read-only (read_only: true)
  • All Linux capabilities are dropped upfront (cap_drop: [ALL])
  • Privilege escalation is blocked (no-new-privileges:true)
  • The only writable paths are the host-mounted state directory and a few tmpfs mounts
  • The runtime image is built on Docker Hardened Image (dhi.io/node:24)

Secrets stay off the image entirely. OPENCLAW_GATEWAY_TOKEN and OPENROUTER_API_KEY are injected at runtime via the host-mounted env file. OAuth tokens for Codex are stored in the host-mounted state directory and never touch the image. The artifact checksum is verified before docker build begins, so you're not trusting a fetched tarball blindly.

That said, there are real limits to what this can guarantee:

  • No amount of hardening eliminates CVEs in the Node runtime or base image
  • A pinned skill SHA is a reproducibility guarantee, not a code review. External skills are third-party code and should be treated accordingly
  • This is a single-trusted-operator model. It is not designed to isolate hostile users from each other
  • Mounted state paths are intentionally durable. A compromised agent or skill can still affect data that lives there

For a fuller threat model, trust boundary definitions, and the per-agent tool and sandbox policy, see docs/security.md.

A machine-readable SBOM in SPDX JSON format is generated from every weekly build candidate and published as a downloadable release artifact. It covers the full container image dependency tree and is regenerated every Monday.


How releases work

There are two branches with distinct roles. The stage branch is the candidate track; successful builds push :candidate to Docker Hub. The main branch is the stable track. A human explicitly triggers promotion, which moves a chosen immutable digest to :stable. Operator machines pull :stable when they upgrade. :latest is intentionally not published — use :stable on production machines and :candidate for pre-promotion validation.

cron (daily) or workflow_dispatch
        │
        ▼
  daily-build.yml
  ├── resolve inputs, verify pinned skills ref
  ├── build immutable candidate (no push)
  └── push :candidate to Docker Hub
        │
        ▼  (human-triggered)
  release-manual.yml  —  retags chosen digest as :stable on Docker Hub and ACR
        │
        ▼
  ./ops/deploy.sh upgrade  —  operators pull :stable

Promotion uses docker buildx imagetools create to retag an immutable digest. No image is rebuilt at promotion time. The bytes operators receive are exactly the bytes that passed CI, nothing more.

Workflow configuration and required secrets are covered in docs/ci-cd.md.


Tests

The suite has three layers testing different things:

Layer Runner Tests What it covers
Python unit tests pytest 66 Upgrade flow, rollback, skills validation, readiness, configure
Node unit tests node:test 23 Env module, upgrade module, TUI field validators
Container integration bash + Docker 9 Entrypoint seeding, /healthz, non-root UID, clean SIGTERM shutdown
make test           # Python + Node unit tests
make test-container # Container integration tests (requires a locally built image)

The container integration tests are worth understanding if you're contributing. Rather than mocking Docker behavior, they start a real container with the correct tmpfs mounts, wait for it to reach a healthy state, exec into it with the right environment to run openclaw config validate and openclaw skills list, and then send SIGTERM and confirm it exits cleanly. They fail if any of the five failing modes we've seen in the wild recur.


Configuration profiles

A profile is how you bundle a specific deployment scenario: which models to expose, which agents are active, which skill set to load, and what the workspace defaults look like. The built-in default profile is a sensible starting point, and most casual users won't need to go further than adjusting it. If you're an advanced user who wants fallback models and a custom agent mix, you can create your own profile by copying and tweaking the default one. The system is designed to support multiple profiles side by side, but that's an advanced use case and not required for most users.

If you do need a separate profile, for a dev vs. prod split or a specialized agent configuration, the path is short:

cp -r config/profiles/default config/profiles/myprofile
./ops/deploy.sh init --profile myprofile

The full guide, including directory layout and available environment variable overrides, is in docs/custom-profiles.md.


Models and auth

Model selection, fallback chains, and credential management are all configured in config/profiles/default/openclaw.json5.example. The full OpenClaw configuration reference is at docs.openclaw.ai/gateway/configuration-reference.

Providers

Four providers are configured out of the box:

Provider key What it is Auth
openai-codex OpenAI Codex via ChatGPT/Codex subscription OAuth (PKCE, free after subscription)
anthropic Direct Anthropic API (built-in) API key (ANTHROPIC_API_KEY)
openrouter OpenRouter proxy for OpenAI and Anthropic models API key (OPENROUTER_API_KEY)
ollama Local models via Ollama's OpenAI-compatible endpoint None (local)

anthropic and openai are built-in providers in OpenClaw. models.mode: "merge" keeps them active alongside the custom openrouter and ollama provider blocks. Remove that line only if you want to disable built-in providers entirely.

Fallback chains

OpenClaw tries models in the fallbacks array in order when the primary is unavailable, rate-limited, or in cooldown. The default chain prefers direct provider access over proxy:

primary:   openai-codex/gpt-5.4               (Codex subscription OAuth)
fallback:  anthropic/claude-sonnet-4-6        (direct Anthropic API + prompt caching)
fallback:  openrouter/anthropic/claude-sonnet-4-6  (single-key catch-all, no ANTHROPIC_API_KEY needed)
fallback:  openrouter/openai/gpt-4o
fallback:  ollama/llama3.3                    (local, no internet required)

The direct anthropic/* fallback is preferred over OpenRouter for Anthropic models because it enables prompt caching (reducing repeat costs) and fast-mode tier control. Set ANTHROPIC_API_KEY to activate it; leave it empty to skip straight to OpenRouter.

The coder agent follows the same primary but falls back to ollama/qwen2.5-coder:32b before the generic models, giving it a local coding-focused fallback when Codex is unavailable.

See docs.openclaw.ai/concepts/model-failover for cooldown rules, billing-disable logic, and how profile rotation interacts with fallbacks.

OpenAI Codex OAuth (subscription auth)

OpenAI explicitly supports Codex subscription OAuth in external tools like OpenClaw. You use subscription capacity rather than API credits, so there is no per-token charge past your existing plan.

The flow runs once on the host, outside the container:

openclaw models auth login --provider openai-codex

This opens a PKCE browser flow and writes the resulting token to:

state/default/openclaw-home/.openclaw/agents/main/agent/auth-profiles.json

That path is host-mounted, so the token survives container replacement and upgrades automatically. When the token expires, OpenClaw rotates to the next fallback and logs a cooldown. Re-run the login command to restore it.

Note

The auth.profiles and auth.order blocks in openclaw.json5 are metadata only. No secrets live in that file. The token itself is always in auth-profiles.json on the host.

Anthropic (direct API)

The anthropic provider is built into OpenClaw. Set ANTHROPIC_API_KEY in env/openclaw.env to activate it. Two features become available that are not available through OpenRouter:

  • Prompt caching — OpenClaw applies a 5-minute cache by default for all anthropic/* requests, reducing costs on repeated context. Set cacheRetention: "long" for a 1-hour cache on supported models.
  • Fast mode — /fast on maps to service_tier: "auto" on direct Anthropic requests.

Thinking defaults — Claude 4.x models use adaptive thinking by default in OpenClaw; override per-agent with params.thinking.

See docs.openclaw.ai/providers/anthropic for the full Anthropic provider reference.

OpenRouter (API key catch-all)

OpenRouter provides access to both Anthropic and OpenAI models under a single API key. Set OPENROUTER_API_KEY in env/openclaw.env to activate it. In the default fallback chain, OpenRouter only activates when both Codex OAuth and the direct Anthropic API are unavailable or unconfigured.

Leave OPENROUTER_API_KEY empty if you are running fully local via Ollama.

Ollama (local models)

Ollama models are reached at http://host.docker.internal:11434/v1. On macOS and Windows, Docker Desktop injects this hostname automatically. On Linux, compose.yaml already includes extra_hosts: ["host.docker.internal:host-gateway"] to handle it.

To add a model, pull it on the host and add a corresponding entry to the ollama.models list in the config:

ollama pull qwen2.5-coder:32b

See docs.openclaw.ai/providers/ollama for the full Ollama provider reference.


Documentation

docs/architecture.md Design principles, component model, seed vs mounted state
docs/build.md Build prerequisites, full pipeline steps, generated artifacts
docs/operations.md Complete day-2 operations reference
docs/cli-reference.md Every subcommand, flags, exit codes, and stability status
docs/security.md Threat model, trust boundaries, per-agent tool policy
docs/ci-cd.md Release model, promotion chain, workflow variables
docs/troubleshooting.md 18 failure scenarios — Symptom, Diagnosis, Fix
docs/custom-profiles.md Custom profile creation, directory layout, env overrides
CHANGELOG.md All notable changes across releases

About

No description, website, or topics provided.

Resources

Security policy

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages