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_${PLATFORM}_${ARCH}.tar.gz from https://github.com/CaptainCore/captaincore/releases/latest/download, extracts the captaincore binary and moves it to /usr/local/bin/captaincore with sudo. It then prints “Run ‘captaincore connect’ to get started.”
The script’s own header comment gives the URL as https://captaincore.io/install.sh while the readme uses the raw GitHub URL. Both appear in the source; the raw GitHub URL is the one that is verifiable from the repository.
The binary alone is not the whole CLI. Several commands exec bash scripts from ~/.captaincore/app/, and remote work runs scripts from ~/.captaincore/lib/remote-scripts/. Clone the repository into place:
git clone https://github.com/CaptainCore/captaincore.git ~/.captaincore/
cp ~/.captaincore/config-sample.json ~/.captaincore/config.json
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
├── lib/remote-scripts/ # bash scripts run on managed sites over SSH
├── 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.
Getting the token
Run captaincore connect and answer three prompts:
captaincore connect
# WordPress URL: https://mysite.com
# Username: yourlogin
# Application password: ****
It POSTs with HTTP Basic Auth to https://mysite.com/wp-json/captaincore/v1/cli/connect. Use a WordPress application password, and the account must have the administrator role (a non-admin gets a 403). The response carries token, api_url, gui_url, plus sites, environments, accounts, providers, domains, junction rows, configurations and defaults.
connect initializes data/captaincore.db, upserts everything into it, and writes keys.token, vars.captaincore_api, vars.captaincore_gui and vars.websites back into config.json. On a re-run against a populated database it prints a sync preview and asks before applying, then warns about any missing rclone, restic or git.
Flags:
| Flag | Effect |
|---|---|
--url |
WordPress site URL, skips the prompt |
--username |
WordPress username, skips the prompt |
--password |
application password, skips the prompt |
--skip-ssl |
skip TLS certificate verification |
--sync |
re-sync using the saved token and API URL, no prompts, no confirmation |
--sync derives the connect endpoint by replacing the trailing /api of vars.captaincore_api with /cli/connect, and authenticates with keys.token in the JSON body instead of Basic Auth. This is the command to run on a schedule.
On the Manager side the same token is what captaincore_get_cli_token() returns, so you never have to copy it by hand.
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 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" }
]
}
This file is separate from ~/.captaincore/config.json, and nothing in the repository generates it. Create it by hand with the same token value the Manager reports, or requests from the Manager resolve to captain id 0.