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:9210Fresh Master installations automatically create a starter user
chiraland 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-projectFor 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_xxxxxThe same temporary URL works for both roles; the CLI sends its role to the Master.
tokens create --type bundlealso writes local TOML files by default fordeploy.py install worker <source>anddeploy.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>ordeploy.py install client <source>explicitly applies that source and replaces the local identity. Usedeploy.py uninstall <component> --purgeonly when you want to remove config and data.After deployment, the
capownandcapown-masterCLI tools are installed into~/.capown/bin/. Add this to yourPATHto use them:Bootstrap Options¶
Fresh Master installations (no existing
~/.capown/master/config.tomlor~/.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-bootstrapThe public URL can also be set via the
MASTER_PUBLIC_URLenvironment variable: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 acceptsMASTER_PUBLIC_URLenv 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:
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.pymanages the runtime automatically viauv(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:
Use --no-bootstrap to skip automatic user and bundle creation:
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:
2. Prepare Data Directory¶
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¶
Port model (Docker). The Master inside the container always listens on
0.0.0.0:9210. Docker publishes this as0.0.0.0:9210on the host by default, so Workers on the same network can reach it viahttp://<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), setCAPOWN_HOST_PORTbefore runningdeploy.pyordocker compose up:This publishes
0.0.0.0:9211on the host while the container still listens on the standard 9210 internally. The same env var must be set when runningcapown-master healthorcapown-master startif 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.1before deployment.
5. Management CLI¶
After deploying the Master, use capown-master to manage the service:
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:
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:
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:
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 examplemaster_url + "/api/workers"), so a/v1prefix inmaster_urlis common. - Configuring only
location /v1/does not expose publichttps://.../mcp; MCP needs its ownlocation(or an equivalent mapping). - Origin validation compares scheme/host/port only and ignores path. Set
public_urlto a pure origin such ashttps://api.example.net, nothttps://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:
- Browser MCP clients send
Origin; it must matchpublic_urlscheme/host/port. - Non-browser MCP clients usually omit
Originand authenticate with a client bearer token. - When
public_urlis unset, every MCP request that includesOriginreturns 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:9210for LAN-reachable quick starts. Use a firewall, or setCAPOWN_BIND_HOST=127.0.0.1and 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 buildand 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:
- Use
DOCKER_BUILDKIT=0 docker build --network=host(legacy builder + host network) - Use a mirror closer to your server via
--build-arg APT_MIRROR=... - Check that the
APT_MIRRORbuild arg is re-declared afterFROMin 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-starlettesends 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\nas 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.