This document defines the fixed deployment process for OpenClaw Cattle Mode. Follow these rules exactly — do not improvise or deviate.
There are four env files. Each has a specific purpose. Do not mix them up.
| File | Purpose | Gitignored | Used by |
|---|---|---|---|
.env.docker |
Local Docker Compose secrets + config | Yes | docker compose --env-file .env.docker |
.env.docker.example |
Template for .env.docker |
No | Developers copy this to .env.docker |
.env.remote |
App Platform secrets + config | Yes | scripts/deploy.sh |
.env.remote.example |
Template for .env.remote |
No | Developers copy this to .env.remote |
- Never commit
.env.dockeror.env.remote— they contain real secrets. - Never hardcode secrets in
app.yaml— it uses${VAR}placeholders. - Never use
.env.dockerfor App Platform — it has local-only values (RustFS, localhost endpoints). - Never use
.env.remotefor Docker Compose — it has production values (DO Spaces, real tokens). - Keep
.env.docker.exampleand.env.remote.examplein sync with any new env vars added toapp.yamlordocker-compose.yml.
# Local development
cp .env.docker.example .env.docker
# Fill in: TELEGRAM_BOT_TOKEN, GRADIENT_API_KEY, OPENCLAW_GATEWAY_TOKEN
# App Platform
cp .env.remote.example .env.remote
# Fill in: TELEGRAM_BOT_TOKEN, GRADIENT_API_KEY, OPENCLAW_GATEWAY_TOKEN,
# SPACES_ACCESS_KEY_ID, SPACES_SECRET_ACCESS_KEY# Gateway token (used in both .env.docker and .env.remote)
openssl rand -hex 32
# Spaces access key (only for .env.remote)
doctl spaces keys create "my-key-name" \
--grants "bucket=<bucket>;permission=readwrite" \
--output json
# Secret key is shown ONLY at creation time — save it immediately.The GHCR image (ghcr.io/bikramkgupta/openclaw-agent:<tag>) is built once and baked at build time. deploy.sh only updates the app spec — it does NOT rebuild the image.
| What changed | Action needed |
|---|---|
openclaw-agent-image/scripts/* (entrypoint, setup, backup, etc.) |
Rebuild image, then deploy |
openclaw-agent-image/Dockerfile |
Rebuild image, then deploy |
openclaw-agent-image/config/* (base JSON template) |
Rebuild image, then deploy |
app.yaml (spec template) |
Deploy only |
.env.remote (env var values) |
Deploy only |
IMAGE_TAG in .env.remote (switch to new image) |
Deploy only |
# 1. Rebuild — triggers GitHub Actions to build and push to GHCR
gh workflow run ghcr-build-push.yml -f openclaw_version=2026.2.12
# 2. Wait for build to complete
gh run watch # or check Actions tab
# 3. Deploy with new image
bash scripts/deploy.shbash scripts/deploy.shEnv file: .env.docker
docker compose --env-file .env.docker up -dDocker Compose reads .env.docker and starts all services locally (agent + RustFS).
docker compose --env-file .env.docker build --no-cache openclaw-agent
docker compose --env-file .env.docker up -ddocker compose --env-file .env.docker logs -f openclaw-agent
docker compose --env-file .env.docker exec openclaw-agent /usr/local/bin/openclaw-healthcheckEnv file: .env.remote
bash scripts/deploy.sh- Reads
.env.remote - Runs
envsubstonapp.yaml→ writes.app-spec-rendered.yaml(temp file) - Finds or creates the app via
doctl apps - Runs
doctl apps update <app-id> --spec .app-spec-rendered.yaml - Deletes
.app-spec-rendered.yamlimmediately (secrets must not persist on disk) - Triggers
doctl apps create-deployment - Waits for deployment to reach ACTIVE or ERROR
bash scripts/deploy.sh --dry-rundoctlinstalled and authenticated (doctl auth init)envsubstinstalled (brew install gettexton macOS).env.remotepopulated with all values
Secrets stored in: GitHub repo Settings → Secrets → Actions
The GHCR workflow builds and pushes the container image:
# Trigger manually
gh workflow run ghcr-build-push.yml -f openclaw_version=2026.2.12This pushes to ghcr.io/bikramkgupta/openclaw-agent with tags: latest, <version>, <sha>.
The Integration workflow (.github/workflows/integration.yml) runs on push/PR and optionally on manual trigger:
- deploy-spec — Renders
app.yamlwith envsubst and checks for unsubstituted variables (no secrets needed). - container-test — Builds and runs the stack with Docker Compose. Creates
.env.dockerfrom GitHub Secrets:- Required:
AGENT_ID,OPENCLAW_GATEWAY_TOKEN(container will not boot without these). - Optional:
TELEGRAM_BOT_TOKEN,GRADIENT_API_KEY. If both are set, the job runs full E2E (test-backup.sh: backup/restore against local RustFS). Otherwise it runs smoke only (smoke-boot.sh: container boots and health check passes).
- Required:
- deploy — Runs only when you trigger the workflow manually and check Run deploy to App Platform. Requires
DIGITALOCEAN_TOKENand every variable from.env.remoteas GitHub Secrets (same list as below). Usesdoctlto update the app spec and create a deployment, then waits for ACTIVE.
Secrets for container-test (minimum for smoke):
| Secret | Required for smoke | Required for full E2E |
|---|---|---|
AGENT_ID |
Yes | Yes |
OPENCLAW_GATEWAY_TOKEN |
Yes | Yes |
TELEGRAM_BOT_TOKEN |
No | Yes |
GRADIENT_API_KEY |
No | Yes |
Secrets for deploy job: DIGITALOCEAN_TOKEN plus all .env.remote vars (e.g. AGENT_ID, AGENT_NAME, TELEGRAM_BOT_TOKEN, TELEGRAM_ALLOWFROM, GRADIENT_API_KEY, AGENT_DEFAULT_MODEL, NODE_OPTIONS, OPENCLAW_GATEWAY_TOKEN, SPACES_BUCKET, SPACES_REGION, SPACES_ACCESS_KEY_ID, SPACES_SECRET_ACCESS_KEY, and optionally IMAGE_TAG).
app.yaml is a template, not a directly deployable spec. It contains ${VAR} placeholders:
envs:
- key: TELEGRAM_BOT_TOKEN
scope: RUN_TIME
value: "${TELEGRAM_BOT_TOKEN}"Never deploy app.yaml directly with doctl apps update --spec app.yaml.
Always use scripts/deploy.sh which substitutes values from .env.remote.
App Platform pulls from GHCR using:
image:
registry_type: GHCR
registry: ghcr.io
repository: bikramkgupta/openclaw-agent
tag: ${IMAGE_TAG}- The tag is set by
IMAGE_TAGin.env.remote(e.g.2026.2.12). Use a pinned tag so deploys don't pulllatestunexpectedly. registry_type: GHCRis required — it handles GHCR's token-exchange auth.DOCKER_HUBdoes not work for GHCR images.- For public GHCR images, no
registry_credentialsneeded. - For private GHCR images, add:
registry_credentials: "<username>:<PAT>"
When adding a new env var, update all of these:
app.yaml— add${NEW_VAR}placeholder in the envs section.env.remote.example— add with blank or default value.env.remote— add with real value.env.docker.example— add with blank or default value (if used locally).env.docker— add with real value (if used locally)docker-compose.yml— add to environment section (if used locally)
The upstream OpenClaw docs live at github.com/openclaw/openclaw/docs and are tagged per release. Use fetch-openclaw-doc.sh to pull docs for the installed version:
# List available doc sections
bash scripts/fetch-openclaw-doc.sh --list
# List docs in a section
bash scripts/fetch-openclaw-doc.sh --list channels
# Fetch a specific doc (version auto-detected from IMAGE_TAG in .env.remote)
bash scripts/fetch-openclaw-doc.sh channels/whatsapp
bash scripts/fetch-openclaw-doc.sh channels/telegram
# Fetch for a specific version
bash scripts/fetch-openclaw-doc.sh channels/whatsapp 2026.2.9
# Other useful sections
bash scripts/fetch-openclaw-doc.sh --list gateway
bash scripts/fetch-openclaw-doc.sh --list security
bash scripts/fetch-openclaw-doc.sh --list plugins
bash scripts/fetch-openclaw-doc.sh --list toolsThe script resolves version in order: explicit argument → IMAGE_TAG from .env.remote → .env.docker → main branch.
| Task | Command |
|---|---|
| Start locally | docker compose --env-file .env.docker up -d |
| Deploy to App Platform | bash scripts/deploy.sh |
| Preview deploy spec | bash scripts/deploy.sh --dry-run |
| Build image (GHCR) | gh workflow run ghcr-build-push.yml |
| Build image (DOCR) | gh workflow run build-push.yml |
| Check deploy logs | doctl apps logs <app-id> --type deploy |
| Check runtime logs | doctl apps logs <app-id> --type run |
| Test deploy spec (no Docker) | bash scripts/test-deploy-spec.sh |
| Test smoke boot | bash scripts/smoke-boot.sh |
| Test backup/restore | bash scripts/test-backup.sh |
| Test version compat | bash scripts/test-versions.sh |