Manage CapRover PaaS instances via API: create/update apps, deploy from Docker image or custom Dockerfile (tar file), configure ports, volumes, env vars, and...
---
name: caprover
description: "Manage CapRover PaaS instances via API: create/update apps, deploy from Docker image or custom Dockerfile (tar file), configure ports, volumes, env vars, and serviceUpdateOverride for Docker Swarm settings. Use when the user wants to deploy, configure, or diagnose an app on a CapRover server — including setting up TCP ports for non-HTTP servers (game servers, databases), mounting persistent volumes, building custom Docker images on the host, or reading build/runtime logs."
---
# CapRover Management Skill
CapRover is a self-hosted PaaS that wraps Docker Swarm. It exposes a REST API for full app lifecycle management.
## Quick Setup
Always start by authenticating:
```python
import urllib.request, json, ssl
ctx = ssl.create_default_context()
ctx.check_hostname = False
ctx.verify_mode = ssl.CERT_NONE # self-signed cert on CapRover is common
BASE = "https://<captain-domain>" # e.g. https://captain.example.com
def api(path, data=None, token=None, timeout=60):
body = json.dumps(data).encode() if data else None
headers = {"Content-Type": "application/json"}
if token:
headers["x-captain-auth"] = token
req = urllib.request.Request(f"{BASE}{path}", data=body, headers=headers)
resp = urllib.request.urlopen(req, context=ctx, timeout=timeout)
return json.loads(resp.read())
token = api("/api/v2/login", {"password": "<password>"})["data"]["token"]
```
See `references/api.md` for all endpoints. See `scripts/caprover.py` for a ready-to-use helper class.
## Core Workflows
### 1. Create an App
```python
api("/api/v2/user/apps/appDefinitions/register",
{"appName": "myapp", "hasPersistentData": False}, token)
```
Set `hasPersistentData: True` if the app needs persistent volumes.
### 2. Deploy from a Docker Image
```python
api("/api/v2/user/apps/appDefinitions/update",
{"appName": "myapp", "imageName": "nginx:latest"}, token)
api("/api/v2/user/apps/appData/myapp/redeploy",
{"appName": "myapp", "gitHash": ""}, token)
```
### 3. Deploy from a Custom Dockerfile (Build on Host)
Pack a `captain-definition`, `Dockerfile`, and support files into a `.tar.gz`, then POST:
```python
# captain-definition (required in tar root):
# {"schemaVersion": 2, "dockerfilePath": "./Dockerfile"}
with open("app.tar.gz", "rb") as f:
tar_data = f.read()
boundary = "----FormBoundaryCaprover"
body = (
f"--{boundary}\r\n"
f'Content-Disposition: form-data; name="sourceFile"; filename="app.tar.gz"\r\n'
f"Content-Type: application/octet-stream\r\n\r\n"
).encode() + tar_data + f"\r\n--{boundary}--\r\n".encode()
req = urllib.request.Request(
f"{BASE}/api/v2/user/apps/appData/myapp",
data=body,
headers={
"Content-Type": f"multipart/form-data; boundary={boundary}",
"x-captain-auth": token,
},
)
resp = urllib.request.urlopen(req, context=ctx, timeout=180)
```
**This builds the image natively on the CapRover host** — critical for ARM64 hosts where pre-built amd64 images won't run.
### 4. Configure Ports, Env Vars, Volumes
```python
api("/api/v2/user/apps/appDefinitions/update", {
"appName": "myapp",
"envVars": [{"key": "MY_VAR", "value": "hello"}],
"ports": [{"hostPort": 25565, "containerPort": 7777}],
"volumes": [{"containerPath": "/data", "volumeName": "myapp-data"}],
"instanceCount": 1,
}, token)
```
> ⚠️ **Port update bug**: The `ports` field update sometimes returns HTTP 500 on CapRover (known issue). Workaround: set ports once at app creation time or use `serviceUpdateOverride`.
### 5. Advanced Docker Swarm Settings (serviceUpdateOverride)
For settings not exposed in the standard API — volume mounts, custom DNS, resource limits:
```python
override = json.dumps({
"TaskTemplate": {
"ContainerSpec": {
"Mounts": [{
"Type": "volume",
"Source": "captain--myapp-data", # CapRover names: captain--<appname>-<name>
"Target": "/data"
}]
}
}
})
api("/api/v2/user/apps/appDefinitions/update",
{"appName": "myapp", "serviceUpdateOverride": override}, token)
```
> ⚠️ Setting `serviceUpdateOverride` to `""` (empty string) **clears** it and removes all Docker Swarm overrides, including volume mounts.
### 6. Read Logs
```python
# Build logs (after deploying)
r = api("/api/v2/user/apps/appData/myapp", token=token)
build_lines = r["data"]["logs"]["lines"]
# Runtime logs (stdout of running container)
r = api("/api/v2/user/apps/appData/myapp/logs", token=token)
raw_logs = r["data"]["logs"]
```
## ARM64 / Multi-Arch Gotchas
If the CapRover host is ARM64 (`uname -m` returns `aarch64`):
- **Do not use amd64-only pre-built images** — they will silently fail or crash with exec format errors
- **Build from Dockerfile on the host** (workflow #3 above) to get native ARM64 images
- For apps that need Mono (e.g. Windows .exe files on Linux ARM64): install `mono-runtime` in the Dockerfile and use `mono ./App.exe` as the entrypoint
- Detect arch at runtime in scripts: `$(uname -m)` returns `aarch64` on ARM64
## Common Issues
| Symptom | Likely Cause | Fix |
|---|---|---|
| `HTTP 500` on port update | CapRover bug | Set ports at app creation, or use serviceUpdateOverride |
| Container crashes, no logs | Wrong arch image (amd64 on arm64) | Build from Dockerfile on host |
| Port open but server not responding | Server listening on `127.0.0.1` only | Check server bind address; use `0.0.0.0` |
| World/data lost on restart | No volume mount | Add `serviceUpdateOverride` with `Mounts` |
| Logs empty | App writes logs to file, not stdout | Override entrypoint to redirect to stdout |
| `volumes: []` in API but data persists | serviceUpdateOverride holds the mount — API and Swarm state diverge | Check serviceUpdateOverride, not just app definition |
## Node / Cluster Info
```python
r = api("/api/v2/user/system/info", token=token)
nodes = r["data"]["nodes"]
```
## References
- Full API endpoint list + request/response shapes: `references/api.md`
- Reusable Python helper class: `scripts/caprover.py`
don't have the plugin yet? install it then click "run inline in claude" again.
deploy, configure, and manage applications on a self-hosted CapRover PaaS instance via REST API. use this skill when you need to create apps, deploy from docker images or build custom dockerfiles on the host, configure networking (ports for http and non-http services), mount persistent volumes, set environment variables, or read build and runtime logs. critical for multi-arch deployments where native image builds are required (e.g., arm64 hosts).
CapRover Instance
CAPROVER_HOST: fully qualified domain (e.g., https://captain.example.com). must be accessible over https. self-signed certificates are common and supported.CAPROVER_PASSWORD: admin password for /api/v2/login endpoint.Network & Auth
App Deployment
nginx:latest) or tar.gz file containing captain-definition and Dockerfilecaptain-definition file with schema version and dockerfile pathcaptain--)External Connections
step 1: authenticate and get token
input: caprover host, admin password
import urllib.request, json, ssl
ctx = ssl.create_default_context()
ctx.check_hostname = False
ctx.verify_mode = ssl.CERT_NONE # self-signed cert is common on caprover
BASE = "https://<captain-domain>" # e.g., https://captain.example.com
def api(path, data=None, token=None, timeout=60):
body = json.dumps(data).encode() if data else None
headers = {"Content-Type": "application/json"}
if token:
headers["x-captain-auth"] = token
req = urllib.request.Request(f"{BASE}{path}", data=body, headers=headers)
try:
resp = urllib.request.urlopen(req, context=ctx, timeout=timeout)
return json.loads(resp.read())
except urllib.error.HTTPError as e:
return {"error": e.code, "msg": e.reason, "body": e.read().decode()}
token = api("/api/v2/login", {"password": "<password>"})["data"]["token"]
output: auth token (string), valid for session
step 2: create app (register)
input: token, app name, persistent data flag
api("/api/v2/user/apps/appDefinitions/register",
{"appName": "myapp", "hasPersistentData": False}, token)
set hasPersistentData: True if the app writes data that must survive restarts (database, file uploads, cache, etc.)
output: app registered in caprover, ready for deployment config
step 3a: deploy from pre-built docker image
input: token, app name, image name (docker hub or registry url)
# set the image
api("/api/v2/user/apps/appDefinitions/update",
{"appName": "myapp", "imageName": "nginx:latest"}, token)
# trigger redeploy
api("/api/v2/user/apps/appData/myapp/redeploy",
{"appName": "myapp", "gitHash": ""}, token)
output: caprover pulls image from registry and runs container, logs available after ~10-30 seconds
step 3b: deploy from custom dockerfile (build on host)
input: token, app name, tar.gz file with dockerfile and captain-definition
pack dockerfile, captain-definition, and any support files into tar.gz:
app.tar.gz root must contain:
captain-definition (json file)
Dockerfile
[any other build context files]
captain-definition format:
{
"schemaVersion": 2,
"dockerfilePath": "./Dockerfile"
}
upload and build:
with open("app.tar.gz", "rb") as f:
tar_data = f.read()
boundary = "----FormBoundaryCaprover"
body = (
f"--{boundary}\r\n"
f'Content-Disposition: form-data; name="sourceFile"; filename="app.tar.gz"\r\n'
f"Content-Type: application/octet-stream\r\n\r\n"
).encode() + tar_data + f"\r\n--{boundary}--\r\n".encode()
req = urllib.request.Request(
f"{BASE}/api/v2/user/apps/appData/myapp",
data=body,
headers={
"Content-Type": f"multipart/form-data; boundary={boundary}",
"x-captain-auth": token,
},
)
resp = urllib.request.urlopen(req, context=ctx, timeout=180)
output: caprover builds image natively on host (critical for arm64 hosts), container deployed after ~30-120 seconds depending on dockerfile complexity
step 4: configure ports, env vars, volumes
input: token, app name, ports list, env vars list, volumes list, instance count
api("/api/v2/user/apps/appDefinitions/update", {
"appName": "myapp",
"envVars": [
{"key": "MY_VAR", "value": "hello"},
{"key": "DB_HOST", "value": "postgres.local"}
],
"ports": [
{"hostPort": 80, "containerPort": 8080}, # http
{"hostPort": 25565, "containerPort": 7777} # tcp (minecraft, game server, etc)
],
"volumes": [
{"containerPath": "/data", "volumeName": "myapp-data"},
{"containerPath": "/var/lib/postgres", "volumeName": "myapp-postgres"}
],
"instanceCount": 1,
}, token)
ports: any hostPort under 1024 requires root. non-http servers (game servers, databases, cache) just list them here without caprover trying to proxy them.
volumes: caprover auto-prefixes volume name with captain--. so volumeName: "myapp-data" becomes captain--myapp-data on the docker swarm side.
output: app config updated, redeploy triggered
step 5: advanced docker swarm settings (serviceUpdateOverride)
input: token, app name, swarm override json (mounts, dns, resource limits, etc.)
for settings not exposed in the standard api, use serviceUpdateOverride:
override = json.dumps({
"TaskTemplate": {
"ContainerSpec": {
"Mounts": [{
"Type": "volume",
"Source": "captain--myapp-data",
"Target": "/data"
}],
"DNSConfig": {
"Nameservers": ["8.8.8.8", "8.8.4.4"]
}
},
"Resources": {
"Limits": {
"MemoryBytes": 536870912 # 512 mb
}
}
}
})
api("/api/v2/user/apps/appDefinitions/update",
{"appName": "myapp", "serviceUpdateOverride": override}, token)
output: swarm task template updated, container restarts with new limits/mounts
step 6: read build logs
input: token, app name
r = api("/api/v2/user/apps/appData/myapp", token=token)
build_lines = r["data"]["logs"]["lines"]
print("\n".join(build_lines))
output: list of build log lines (stdout + stderr from dockerfile build)
step 7: read runtime logs
input: token, app name
r = api("/api/v2/user/apps/appData/myapp/logs", token=token)
raw_logs = r["data"]["logs"]
print(raw_logs)
output: raw string of container stdout/stderr
step 8: query node and cluster info
input: token
r = api("/api/v2/user/system/info", token=token)
nodes = r["data"]["nodes"]
print(f"nodes: {len(nodes)}, arch: {nodes[0].get('architecture', 'unknown')}")
output: dict with node count, architecture (x86_64 or aarch64), docker version, caprover version
if deploying pre-built image vs custom dockerfile
if app needs persistent data
hasPersistentData: True at step 2 if app writes user data, database files, cache, logs, etc. that must survive container restarts.False if app is stateless (nginx, api gateway, cache that can rebuild).if port update fails with http 500
ports field sometimes returns 500. workaround: set ports once at app creation (step 2) and do not update later. alternatively, use serviceUpdateOverride to manage ports