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.