Skip to content

CapOwn - Manual Deployment Guide

First install? Follow Getting Started for the four-step path (Master → Worker → token → agent). This page covers advanced deployment, Nginx, and troubleshooting.

Tip: The deployment script (python3 deploy.py) supports both interactive and source-driven deployment:

# Interactive Master deployment (first-time setup)
python3 deploy.py install master --public-url http://192.168.1.10:9210

Fresh Master installations automatically create a starter user chiral and a bundle enrollment token. The generated install commands appear in the output after deployment.

# One-step deployment from a source (local TOML, TOML URL, or bundle enrollment URL)
python3 deploy.py install worker https://master.example.com/api/enroll/cown_tmp_xxxxx
python3 deploy.py install worker capown-worker.toml
python3 deploy.py install worker capown-worker.toml --mirror-cn   # China mirrors
python3 deploy.py install client https://master.example.com/api/enroll/cown_tmp_xxxxx
python3 deploy.py install client capown-client.toml

# With uv mirror for restricted networks
python3 deploy.py install worker --uv-mirror https://pypi.example.com/simple
CAPOWN_UV_MIRROR_URL=https://pypi.example.com/simple python3 deploy.py install worker

# With custom workspace path (overrides enrollment config defaults)
python3 deploy.py install worker capown-worker.toml --workspace /srv/my-project

For multi-user setups, generate a bundle enrollment URL on the Master host:

# On the Master host
capown-master tokens create alice

# Or with explicit URL (overrides configured public_url)
capown-master tokens create alice --type bundle --master-url https://master.example.com

# On the user's Worker machine: one-step install + identity
python3 deploy.py install worker https://master.example.com/api/enroll/cown_tmp_xxxxx

# Or already installed -- reconfigure identity only
capown-worker config https://master.example.com/api/enroll/cown_tmp_xxxxx

# On the user's Client/AI-Agent machine: one-step install + identity
python3 deploy.py install client https://master.example.com/api/enroll/cown_tmp_xxxxx

# Or already installed -- reconfigure identity only
capown config https://master.example.com/api/enroll/cown_tmp_xxxxx

The same temporary URL works for both roles; the CLI sends its role to the Master. tokens create --type bundle also writes local TOML files by default for deploy.py install worker <source> and deploy.py install client <source> workflows. Only one active bundle URL is allowed per user -- generating a new one automatically revokes the previous one.

Re-running an install command without a source updates software and preserves existing local config. Running deploy.py install worker <source> or deploy.py install client <source> explicitly applies that source and replaces the local identity. Use deploy.py uninstall <component> --purge only when you want to remove config and data.

After deployment, the capown and capown-master CLI tools are installed into ~/.capown/bin/. Add this to your PATH to use them:

export PATH="$PATH:$HOME/.capown/bin"

Bootstrap Options

Fresh Master installations (no existing ~/.capown/master/config.toml or ~/.capown/master/data/registry.db) automatically create a starter user and enrollment bundle.

# Default: user 'chiral' is created automatically
python3 deploy.py install master --public-url http://192.168.1.10:9210

# Custom username
python3 deploy.py install master --public-url http://192.168.1.10:9210 --bootstrap-user admin

# Skip automatic bootstrap (install without public URL)
python3 deploy.py install master --no-bootstrap

The public URL can also be set via the MASTER_PUBLIC_URL environment variable:

MASTER_PUBLIC_URL=http://192.168.1.10:9210 python3 deploy.py install master

Bootstrap options: - --bootstrap-user <name>: username for the starter user (default: chiral) - --no-bootstrap: skip automatic user and bundle creation - --public-url <url>: public Master URL (also accepts MASTER_PUBLIC_URL env var)

Bootstrap only runs on a fresh installation. Reinstalling or upgrading an existing Master never creates, replaces, or revokes users or tokens. The starter bundle uses a 24-hour TTL. If bootstrap fails, the Master stays running and a recovery command is printed.

To create additional users after deployment:

capown-master tokens create <username>

This guide covers manual setup for advanced use cases.

Prerequisites

  • Docker and Docker Compose installed on the Master host
  • Outbound HTTPS access from all Worker hosts to the Master
  • Python 3.10+ is required to run deploy.py. No manual Python 3.12 installation is needed -- deploy.py manages the runtime automatically via uv (installed to ~/.capown/tools/uv/). Python 3.12 is downloaded and managed by uv on first deploy. You do not need to pre-install Python package dependencies; the deploy script creates virtual environments and installs runtime dependencies automatically.

To use a network mirror (for China or air-gapped environments):

# Via environment variable (applies to all install commands)
export CAPOWN_UV_MIRROR_URL=https://pypi.tuna.tsinghua.edu.cn/simple

# Or via CLI flag (per-command)
python deploy.py install worker --uv-mirror https://pypi.tuna.tsinghua.edu.cn/simple

Master Deployment (Container)

1. Deploy the Master

Deploy the Master using the deployment script. On a fresh installation, the starter user chiral and a bundle enrollment token are created automatically.

# Interactive deployment (prompts for settings)
python3 deploy.py install master --public-url http://192.168.1.10:9210

# Non-interactive deployment with defaults
python3 deploy.py install master --public-url http://192.168.1.10:9210 -y

A --public-url is required for bootstrap on fresh installations. It can be supplied via the MASTER_PUBLIC_URL environment variable:

MASTER_PUBLIC_URL=http://192.168.1.10:9210 python3 deploy.py install master -y

Use --no-bootstrap to skip automatic user and bundle creation:

python3 deploy.py install master --no-bootstrap

After deployment, the generated install commands for Workers and Clients are printed. The starter bundle is valid for 24 hours by default.

To issue additional enrollment credentials on the Master host:

capown-master tokens create <username>

2. Prepare Data Directory

mkdir -p ~/.capown/master/data

3. Build and Run

cd master/

# Build with defaults (international mirrors)
DOCKER_BUILDKIT=0 docker compose build

# Or with China mirrors
DOCKER_BUILDKIT=0 \
  APT_MIRROR=mirrors.tuna.tsinghua.edu.cn \
  PIP_INDEX_URL=https://pypi.tuna.tsinghua.edu.cn/simple \
  docker compose build

# Start the container
CAPOWN_MASTER_CONFIG="$HOME/.capown/master/config.toml" \
  CAPOWN_MASTER_DATA="$HOME/.capown/master/data" \
  docker compose up -d

# Check status
docker compose logs master

4. Verify

curl http://127.0.0.1:9210/health

Port model (Docker). The Master inside the container always listens on 0.0.0.0:9210. Docker publishes this as 0.0.0.0:9210 on the host by default, so Workers on the same network can reach it via http://<host-ip>:9210. Make sure the host firewall allows inbound TCP 9210. To use a different host-side port (e.g. when 9210 is already taken), set CAPOWN_HOST_PORT before running deploy.py or docker compose up:

CAPOWN_HOST_PORT=9211 python3 deploy.py install master

This publishes 0.0.0.0:9211 on the host while the container still listens on the standard 9210 internally. The same env var must be set when running capown-master health or capown-master start if a non-default host port is used.

To keep the Master port local-only for an Nginx/reverse-proxy setup, set CAPOWN_BIND_HOST=127.0.0.1 before deployment.

5. Management CLI

After deploying the Master, use capown-master to manage the service:

capown-master status
capown-master health
capown-master logs

See the user guide for all available commands.

Worker Deployment

The Worker control process always runs on the host as a native OS service. When using the container execution backend, a managed Docker container is additionally set up for task isolation. The deploy.py script handles both steps automatically.

Host Execution Backend

Tasks run directly on the host system. Suitable for trusted machines where Docker is not needed.

1. Write Config File

Create ~/.capown/worker/config.toml:

[worker]
execution_mode = "host"
worker_name = "<unique-worker-name>"
master_url = "https://your-server.com/v1"
workspace = "/"
max_runtime = 86400
idle_timeout = 600
reconnect_interval = 5

enrollment_token = "cown_enroll_xxx"

2. Install Application Copy

mkdir -p ~/.capown/worker/app
cp -r ../shared ~/.capown/worker/app/
cp -r ../worker ~/.capown/worker/app/
cp ../worker/requirements.txt ~/.capown/worker/app/

3. Virtual Environment (managed by uv)

The virtual environment is created automatically by the deploy script using uv:

# uv is installed automatically, no manual Python setup needed
python3 deploy.py install worker

When running the deploy script, uv is first installed to ~/.capown/tools/uv/ (if not already present on PATH), then Python 3.12 is downloaded via uv, and a virtual environment is created at ~/.capown/worker/venv/ with all dependencies installed.

To use a mirror for uv operations:

CAPOWN_UV_MIRROR_URL=https://pypi.tuna.tsinghua.edu.cn/simple \
  python3 deploy.py install worker

4. Install systemd Service

Create ~/.config/systemd/user/capown-worker.service:

[Unit]
Description=CapOwn Worker
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
ExecStart=%h/.capown/worker/venv/bin/python -m worker.daemon
Restart=always
RestartSec=5
StandardOutput=journal
StandardError=journal

[Install]
WantedBy=default.target

Then enable and start:

systemctl --user daemon-reload
systemctl --user enable --now capown-worker

# Enable linger so the service starts on boot
sudo loginctl enable-linger $USER

Host Execution Backend (Windows)

1. Write Config File

Same as Linux host mode above. Place it at %USERPROFILE%\.capown\worker\config.toml.

2. Install Application Copy

mkdir "$env:USERPROFILE\.capown\worker\app" -Force
Copy-Item ..\shared "$env:USERPROFILE\.capown\worker\app\" -Recurse
Copy-Item ..\worker "$env:USERPROFILE\.capown\worker\app\" -Recurse
Copy-Item ..\worker\requirements.txt "$env:USERPROFILE\.capown\worker\app\"

3. Virtual Environment (managed by uv)

The virtual environment is created automatically by the deploy script:

python deploy.py install worker

uv and Python 3.12 are managed automatically.

4. Create Scheduled Task

# Remove existing task if any
schtasks /Delete /TN CapOwnWorker /F

# Create new task
schtasks /Create `
  /TN CapOwnWorker `
  /SC ONLOGON `
  /TR "$env:USERPROFILE\.capown\worker\venv\Scripts\python -m worker.daemon" `
  /RL LIMITED `
  /F

# Start the task
schtasks /Run /TN CapOwnWorker

Same-machine Master + Worker

When Master and Worker run on the same host, configure the Worker to connect via localhost:

# worker/config.toml
[worker]
execution_mode = "container"  # or "host"
worker_name = "local-worker"
master_url = "http://127.0.0.1:9210"
# ...

Configure Nginx

A public Master that must support both CLI/skill and MCP needs two separate entry points:

Use Public URL example Backend path Client configuration
REST / CLI / skill / Worker SSE / enrollment https://api.example.net/v1/... /api/..., /health, ... CLI: master_url = "https://api.example.net/v1"
MCP Streamable HTTP https://api.example.net/mcp /mcp MCP host: url = "https://api.example.net/mcp"

Notes:

  • On the Master process, REST lives under /api/* and MCP is mounted at the root path /mcp (not under /api).
  • The CLI joins paths onto master_url (for example master_url + "/api/workers"), so a /v1 prefix in master_url is common.
  • Configuring only location /v1/ does not expose public https://.../mcp; MCP needs its own location (or an equivalent mapping).
  • Origin validation compares scheme/host/port only and ignores path. Set public_url to a pure origin such as https://api.example.net, not https://api.example.net/v1.

Merge the following into your HTTPS server block:

# CLI / skill / Worker / enrollment: public prefix /v1 -> backend root
location /v1/ {
    proxy_pass http://127.0.0.1:9210/;
    proxy_http_version 1.1;
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;

    # Worker SSE requires disabled buffering
    proxy_buffering off;
    proxy_cache off;
    proxy_read_timeout 3600s;
    proxy_send_timeout 3600s;

    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";
}

# Optional health check
location = /v1/health {
    proxy_pass http://127.0.0.1:9210/health;
    proxy_set_header Host $host;
    proxy_set_header X-Forwarded-Proto $scheme;
}

# MCP Streamable HTTP: separate location (buffering/timeouts differ from REST)
location /mcp {
    proxy_pass http://127.0.0.1:9210/mcp;
    proxy_http_version 1.1;
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_buffering off;
    proxy_cache off;
    proxy_set_header Connection '';
    chunked_transfer_encoding on;
    proxy_read_timeout 3600s;
    proxy_send_timeout 3600s;
}

Then reload: sudo nginx -t && sudo systemctl reload nginx

Master-side Origin configuration example:

[master]
public_url = "https://api.example.net"
  • Browser MCP clients send Origin; it must match public_url scheme/host/port.
  • Non-browser MCP clients usually omit Origin and authenticate with a client bearer token.
  • When public_url is unset, every MCP request that includes Origin returns 403.

Optional: to keep MCP under /v1, add a more specific location /v1/mcp with proxy_pass pointed at backend /mcp, and configure the MCP host with https://api.example.net/v1/mcp. Prefer a separate location from REST so Streamable HTTP buffering and timeouts stay explicit.

Tool, auth, and client examples: MCP Guide. Chinese: switch site language or see docs/zh/mcp.md.

Security

  • Dual token scheme: Worker Token (worker identity) and Client Token (dispatch authority) are separate
  • All-outbound networking: Workers never expose inbound ports
  • Path traversal blocked: All file operations enforce workspace boundary via realpath validation
  • Container isolation: Both Master and Worker run as non-root in Docker containers
  • Command timeout: Long-running commands are killed automatically
  • No hardcoded secrets: Tokens are loaded from local config files or environment overrides
  • Master port exposure: By default Docker publishes Master on 0.0.0.0:9210 for LAN-reachable quick starts. Use a firewall, or set CAPOWN_BIND_HOST=127.0.0.1 and expose it through Nginx/reverse proxy for production.

Build System

Docker build requirements

Two flags are REQUIRED to build on machines where Docker's default bridge network has no DNS resolution (common on cloud VMs):

Flag Why
DOCKER_BUILDKIT=0 BuildKit's --network=host does not work reliably; the legacy builder passes host networking through to intermediate containers
--network=host Lets the build container use the host's network stack so apt-get and pip can reach external repos

ARG scoping (Dockerfile)

ARGs declared before FROM are only visible in the FROM instruction. To use them in RUN steps, re-declare them after FROM (without default values):

ARG APT_MIRROR=deb.debian.org
FROM ${PYTHON_IMAGE}
ARG APT_MIRROR          # re-declare to bring into scope
RUN sed -i "s|http://deb.debian.org|http://${APT_MIRROR}|g" ...

Sudo and environment variables

sudo strips most environment variables by default. When running docker compose build with sudo, env vars like APT_MIRROR never reach the docker-compose process. Either:

  • Pass them explicitly: sudo -E env APT_MIRROR=... docker compose build
  • Or build directly with docker build and pass --build-arg

Mirror configuration

Build arg Default (international) China example
PYTHON_IMAGE python:3.12-slim (keep default, Docker Hub works)
APT_MIRROR deb.debian.org mirrors.tuna.tsinghua.edu.cn
PIP_INDEX_URL https://pypi.org/simple https://pypi.tuna.tsinghua.edu.cn/simple

To switch to China mirrors, set the environment variables before building:

export DOCKER_BUILDKIT=0
export APT_MIRROR=mirrors.tuna.tsinghua.edu.cn
export PIP_INDEX_URL=https://pypi.tuna.tsinghua.edu.cn/simple
docker compose build

Rebuild after code changes

cd <project-root>
DOCKER_BUILDKIT=0 docker build --no-cache --network=host \
  --build-arg APT_MIRROR=mirrors.tuna.tsinghua.edu.cn \
  --build-arg PIP_INDEX_URL=https://pypi.tuna.tsinghua.edu.cn/simple \
  -t capown-master -f master/Dockerfile .
cd master/ && docker compose up -d
# Repeat for worker/

Troubleshooting

Build fails with "Undetermined Error" (apt-get)

The build container cannot reach the Debian repositories. Three fixes:

  1. Use DOCKER_BUILDKIT=0 docker build --network=host (legacy builder + host network)
  2. Use a mirror closer to your server via --build-arg APT_MIRROR=...
  3. Check that the APT_MIRROR build arg is re-declared after FROM in the Dockerfile (ARG scoping rule)

Build fails with "deb.debian.org" in logs even though mirror is set

The --build-arg value is not reaching the RUN instruction. Most likely the ARG is declared before FROM but not re-declared inside the stage. See "ARG scoping" in the Build System section above.

Worker cannot connect to Master

  • Verify Master is reachable: curl <MASTER_URL>/health
  • Check worker token matches the Master configuration
  • Ensure outbound HTTPS is not blocked by firewall

Task dispatched but no result / Worker shows no "executing task" log

  • Check if the target worker is online: GET /api/workers
  • Verify the worker SSE connection is active (Master logs show "broker: node X connected")
  • Check worker logs:
  • Container mode: docker compose logs worker
  • Linux host mode: journalctl --user -u capown-worker -f
  • Windows host mode: schtasks /Query /TN CapOwnWorker
  • SSE line-ending mismatch: sse-starlette sends events separated by \r\n\r\n (CRLF, per the SSE spec). If the worker splits the stream by \n\n (LF only), events will never be parsed. The daemon must use \r\n\r\n as the event delimiter, or normalize line endings first.
  • Same-machine worker: prefer master_url = "http://127.0.0.1:9210" to avoid SSE streaming issues through the public IP / NAT path.
  • Blocking HTTP client: urllib.request.urlopen().read() may hang indefinitely on SSE streams served by uvicorn. Use an async HTTP client (httpx, aiohttp) with streaming support instead.