Install the CLI
Install the Go binary and the runtime scripts, fill in config.json, and connect the CLI to your Manager.
Quick install
curl -fsSL https://raw.githubusercontent.com/CaptainCore/captaincore/master/install.sh | bash
install.sh detects your platform and architecture, downloads captaincore_<Os>_<Arch>.tar.gz and checksums.txt from the latest GitHub release, verifies the sha256, extracts the captaincore binary and installs it to /usr/local/bin/captaincore, using sudo only when that directory is not writable. It finishes by printing captaincore version and “Run ‘captaincore connect’ to get started.”
| Setting | Effect |
|---|---|
INSTALL_DIR=/path |
install somewhere other than /usr/local/bin |
CAPTAINCORE_VERSION=v1.0.0 |
install a specific release instead of the latest |
CAPTAINCORE_FORCE=1 or --force |
install even when ~/.captaincore is a git checkout |
Flags go after bash -s --, for example curl -fsSL ... | bash -s -- --force.
The binary is the whole CLI. The bash scripts under ~/.captaincore/app/ and ~/.captaincore/lib/ are embedded in it and unpacked on first run, stamped with the binary version in ~/.captaincore/.assets-version. A later captaincore upgrade refreshes them the same way. If ~/.captaincore is a git checkout the binary never touches those files, since the checkout owns them.
Build from source
cd ~/.captaincore
go build -o captaincore .
go.mod requires Go 1.25 or newer. The version string can be set at build time:
go build -ldflags "-X github.com/CaptainCore/captaincore/version.Version=2.0.0" -o captaincore .
Directory layout
~/.captaincore/
├── cmd/ # Cobra command definitions
├── models/ # GORM models over the local SQLite database
├── config/ # config.json parsing
├── providers/ # hosting provider integrations
├── apiclient/ # HTTP client for the Manager API
├── server/ # HTTP + WebSocket server
├── app/ # bash scripts for operations (unpacked from the binary)
├── lib/remote-scripts/ # bash scripts run on managed sites over SSH (unpacked from the binary)
├── .assets-version # which binary version unpacked app/ and lib/
├── config.json # CLI configuration
└── data/
├── captaincore.db # SQLite, chmod 0600, WAL mode
└── config.json # server auth tokens
config.json
config.json is a JSON array. The first entry holds the shared system block. Each further entry is one tenant, keyed by captain_id.
[
{
"system": {
"captaincore_fleet": "false",
"captaincore_dev": "false",
"captaincore_master": "[email protected]",
"captaincore_master_port": "12345",
"logs": "/home/username/Logs",
"path": "/home/username/Sites",
"path_tmp": "/home/username/Tmp",
"path_scripts": "/home/username/Scripts",
"rclone_cli_backup": "B2:Backup/Services/core",
"local_wp_db_pw": "###########"
}
},
{
"captain_id": "1",
"keys": { "token": "###########" },
"remotes": { "rclone_backup": "B2:Backup/Sites" },
"vars": { "captaincore_api": "https://mysite.com/wp-json/captaincore/v1/api" }
}
]
system
The typed fields in config/config.go are:
| Field | Meaning |
|---|---|
captaincore_fleet |
"true" turns on multi-tenant fleet mode |
captaincore_dev |
any value other than empty or "false" skips TLS verification |
captaincore_master |
SSH target for the master server |
captaincore_master_port |
SSH port for the master server |
logs |
log directory |
path |
root directory for local site copies |
path_tmp |
scratch directory |
path_scripts |
scripts directory |
path_keys |
key storage directory |
path_recipes |
recipe storage directory |
rclone_backup |
rclone remote for site backups |
rclone_cli_backup |
rclone remote for backing up the CLI itself |
rclone_snapshot |
rclone remote for snapshots |
rclone_upload |
rclone remote for uploads |
rclone_upload_uri |
public base URI for uploads |
fathom_api_key |
Fathom analytics key |
local_wp_db_pw |
password for the local database |
captaincore_standby |
standby mode flag |
Unknown keys inside system survive a rewrite. LoadConfigFrom() keeps the raw map alongside the typed struct, and CaptainConfig.MarshalJSON() merges the two on save, so fields the Go struct does not model (the sample’s rclone_archive, path_email_health and rclone_logs, for example) are not dropped.
keys, remotes and vars
keys holds secrets. The sample uses access_key, token, auth, b2_account_id, b2_account_key and b2_bucket_id. keys.token is the Manager’s CLI token and is what captaincore connect writes.
remotes holds rclone destinations: rclone_archive, rclone_backup, rclone_logs, rclone_snapshot, b2_snapshots.
vars holds per-tenant values. The sample includes captaincore_branding_name, captaincore_branding_title, captaincore_branding_author, captaincore_branding_author_uri, captaincore_branding_slug, captaincore_server, captaincore_tracker, captaincore_gui, captaincore_api, captaincore_admin_email, captaincore_admin_user and websites. websites is a space-separated list of site slugs and is rewritten by captaincore connect from the active sites the Manager returned.
Inspect the resolved values at any time:
captaincore config fetch # all KEY=VALUE pairs
captaincore config fetch vars captaincore_api # one value
captaincore config fetch keys # a whole section as JSON
captaincore config fetch-captain-ids # space-separated captain ids
When captaincore_fleet is "true", config fetch appends the captain id to path, rclone_backup, rclone_logs and rclone_snapshot.
Connect to the Manager
One command pairs both directions. Run it on the CLI server with a WordPress application password for an administrator account:
captaincore connect --url=https://mysite.com --username=admin --server-url=https://captaincore-api.example.com
# Application password: ****
Leave out any flag and connect prompts for it. --server-url is the public address of captaincore server on this machine; leave it blank at the prompt if this machine will not run the server.
What one run does:
- Authenticates with HTTP Basic Auth to
/wp-json/captaincore/v1/cli/connect(falling back to?rest_route=on a site still on plain permalinks). A non-administrator gets a 403. - Receives the CLI token plus every site, environment, account, provider, domain and setting, and writes them into
data/captaincore.db. - Writes the token and the Manager’s API and dashboard URLs into
config.json, and the token intodata/config.jsonsocaptaincore serveraccepts the Manager’s requests. Both files are mode0600. - Registers the
--server-urlwith the Manager and reports what the Manager will dispatch to, or that awp-config.phpconstant is overriding it. - Warns about a missing
rclone,resticorgit.
A Manager with no sites yet is fine; the CLI counts itself connected as soon as the settings land. On a re-run against a populated database connect prints a sync preview and asks before applying.
| Flag | Effect |
|---|---|
--url |
WordPress site URL, skips the prompt |
--username |
WordPress username, skips the prompt |
--password |
application password, skips the prompt (prefer the prompt, so it stays out of shell history) |
--server-url |
public URL of this CLI server, registered with the Manager for job dispatch |
--skip-ssl |
skip TLS certificate verification, for local sites with self-signed certificates |
--sync |
re-sync using the saved token and API URL, no prompts, no confirmation; never changes the registered server URL |
--sync derives the connect endpoint from vars.captaincore_api and authenticates with keys.token in the JSON body instead of Basic Auth. This is the command to run on a schedule.
Running the server
The Manager dispatches commands to captaincore server:
captaincore server
It binds :8000 by default. Set CAPTAINCORE_SERVER_BIND=127.0.0.1:8000 to pin it to loopback behind a TLS-terminating reverse proxy.
The dashboard opens a websocket straight from the browser to this server for live command output. The server refuses browser connections from any origin it does not know. captaincore connect records the Manager’s dashboard origin in data/config.json (an origins list next to tokens), so the normal pairing covers this. To allow additional dashboards, such as white-label portals on their own domains, list them in CAPTAINCORE_SERVER_ORIGINS (comma-separated), for example as a systemd drop-in:
[Service]
Environment=CAPTAINCORE_SERVER_ORIGINS=https://mysite.com
When an origin is missing, commands dispatched from that dashboard sit at Queued and the server log shows websocket upgrade refused for origin. Server-to-server calls from the Manager carry no Origin header and are unaffected.
The server authenticates each request against a token header, matched against the tokens array in ~/.captaincore/data/config.json:
{
"tokens": [
{ "captain_id": "1", "token": "the-token-from-the-manager" }
]
}
captaincore connect writes this file (mode 0600) with the token it received, preserving any other keys already present. If it is missing or holds a different token, requests from the Manager resolve to captain id 0 and are refused.