@wpelevator/tug 0.12.0 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/README.md +7 -3
  2. package/dist/backup-restore.d.ts +1 -1
  3. package/dist/backup-restore.js +3 -1
  4. package/dist/backup-scheduler.js +3 -1
  5. package/dist/cli-build.d.ts +1 -1
  6. package/dist/cli-build.js +4 -1
  7. package/dist/cli-process.d.ts +13 -0
  8. package/dist/cli-process.js +46 -0
  9. package/dist/cli-progress.d.ts +11 -0
  10. package/dist/cli-progress.js +37 -0
  11. package/dist/cli-prompt.js +5 -4
  12. package/dist/cli-selection.d.ts +13 -0
  13. package/dist/cli-selection.js +39 -0
  14. package/dist/commands/backup.js +48 -31
  15. package/dist/commands/context.d.ts +2 -5
  16. package/dist/commands/context.js +50 -28
  17. package/dist/commands/control.js +55 -42
  18. package/dist/commands/credential.js +21 -2
  19. package/dist/commands/deploy.js +97 -70
  20. package/dist/commands/hold.js +12 -4
  21. package/dist/commands/host.d.ts +2 -2
  22. package/dist/commands/host.js +13 -8
  23. package/dist/commands/node.d.ts +1 -0
  24. package/dist/commands/node.js +103 -71
  25. package/dist/commands/observe.js +7 -2
  26. package/dist/commands/restore.js +71 -50
  27. package/dist/commands/secret.js +8 -3
  28. package/dist/commands/selection.d.ts +22 -0
  29. package/dist/commands/selection.js +90 -0
  30. package/dist/commands/service.js +17 -6
  31. package/dist/commands/volume.js +3 -2
  32. package/dist/control.js +95 -47
  33. package/dist/credentials.js +5 -0
  34. package/dist/db-backup-destination.d.ts +14 -0
  35. package/dist/db-backup-destination.js +28 -0
  36. package/dist/db-locks.d.ts +10 -0
  37. package/dist/db-locks.js +23 -0
  38. package/dist/db-transaction.d.ts +3 -0
  39. package/dist/db-transaction.js +24 -0
  40. package/dist/image.d.ts +9 -0
  41. package/dist/image.js +19 -0
  42. package/dist/util.d.ts +2 -0
  43. package/dist/util.js +6 -0
  44. package/docs/access-control.md +166 -0
  45. package/docs/architecture.md +254 -0
  46. package/docs/backups.md +160 -0
  47. package/docs/cli.md +182 -0
  48. package/docs/compose.md +245 -0
  49. package/docs/configuration.md +52 -0
  50. package/docs/control-plane.md +112 -0
  51. package/docs/development.md +140 -0
  52. package/docs/getting-started.md +75 -0
  53. package/docs/healthchecks.md +51 -0
  54. package/docs/migration.md +113 -0
  55. package/docs/networking.md +150 -0
  56. package/docs/nodes.md +143 -0
  57. package/docs/observability.md +213 -0
  58. package/docs/real-host-evaluation.md +224 -0
  59. package/docs/releases.md +39 -0
  60. package/docs/roadmap.md +234 -0
  61. package/docs/security.md +198 -0
  62. package/docs/service-placement.md +72 -0
  63. package/docs/standalone-binary.md +79 -0
  64. package/docs/swarm-comparison.md +197 -0
  65. package/docs/terminology.md +74 -0
  66. package/docs/testing.md +254 -0
  67. package/docs/troubleshooting.md +33 -0
  68. package/package.json +6 -3
package/docs/cli.md ADDED
@@ -0,0 +1,182 @@
1
+ # CLI reference
2
+
3
+ Run `tug --help` to list commands and `tug <command> --help` for command options, units and defaults. Commands read `tug <subject> <verb> [params]`, such as `tug node add` and `tug secret set`, and `tug <subject> --help` lists a subject's verbs. Everyday project commands (`deploy`, `status`, `logs`, `exec` and the like) stay at the top level, and `tug setup` is a shortcut for `tug control setup`. There are no colon names or dash-joined legacy names.
4
+
5
+ | Subject | Verbs |
6
+ |---|---|
7
+ | (top level) | `setup`, `init`, `deploy` (and `deploy cancel`), `status`, `rollback`, `restart`, `scale`, `logs`, `exec`, `run`, `ps`, `inspect`, `events`, `doctor` |
8
+ | `control` | `setup`, `backup`, `restore` |
9
+ | `node` | `add`, `list` (`ls`), `replace`, `repair`, `remove`, `disable`, `ingress`, `address`, `register`, `enroll` |
10
+ | `secret` | `set`, `list`, `remove` |
11
+ | `backup` | `run`, `list`, `password`, `destination set`, `destination show`, `destination remove` |
12
+ | `restore` | `run`, `list`, `resume`, `release` |
13
+ | `credential` | `create`, `list`, `revoke` |
14
+ | `volume` | `list`, `remove` |
15
+ | `hold` | `release` |
16
+ | `image` | `prune` |
17
+
18
+ ## Arguments, options and configuration
19
+
20
+ A positional argument names the primary target: the SSH host for `node add`, node for `node register`, service for `logs`, secret name for `secret set` or backup archive for `control restore`. Named options qualify the operation. For example, `node add root@app1.example.com --name app1` distinguishes the SSH destination from the registered node name. Commands reject unknown options and extra positional arguments instead of ignoring typos. Numeric options accept bounded integers; invalid values and contradictory options fail before an API request or Docker/SSH operation.
21
+
22
+ | Global option | Environment | Config key | Default |
23
+ |---|---|---|---|
24
+ | `--config <file>` | `TUG_CONFIG` | — | `tug.yml`, `tug.yaml`, `tug.json` or `.tug.json` in the current directory |
25
+ | `--control-url <url>` (operator API endpoint) | `TUG_CONTROL_URL` | `controlUrl` | the credential's default address (an override is a connect target only) |
26
+ | `--project <name>` | `TUG_PROJECT` | `project` | `site` |
27
+ | `--credential <file>` | `TUG_CREDENTIAL` (inline) or `TUG_CREDENTIAL_FILE` | `credential` (a file path), only in the untracked overlay | No credential |
28
+
29
+ Global options can appear before or after a command. Explicit options take precedence over environment variables, then the overlay, then the tracked config, then defaults. Relative `--config` paths resolve from the working directory. A missing default config is allowed; an explicitly selected missing file, invalid YAML or JSON, an unknown key or an invalid value is an error. `init` can create a new selected file. `--project` applies only to project-scoped operations; status and node operations retain their control-wide scope.
30
+
31
+ Most commands print the API result as JSON; `status` prints a readable summary unless given `--json`. `init` prints the written config path, `exec` and `logs` write container output to stdout/stderr, and enrollment prints a runnable command or cloud-init document. `ps` and `inspect` print live Docker data as JSON. `node enroll --json` returns JSON with `command`, and also `cloudInit` with `--install-docker`; its stdout contains no explanatory prose. There is no global `--json` flag. Install preflight and image-transfer tools may also print progress. Errors go to stderr and exit nonzero.
32
+
33
+ Credentials are never command-line arguments, since arguments can appear in process listings and shell history; use `TUG_CREDENTIAL`, `TUG_CREDENTIAL_FILE` or `tug init`. `secret set --value` has the same exposure; omit it to read the value from stdin. The credential path belongs in the untracked overlay (see below), never in the tracked config.
34
+
35
+ `tug deploy` prints progress to stderr in terminals and CI, including with `--json`: source preparation, upload/control preparation, per-service build/export/cleanup for `--build-local`, and deployment state changes while waiting. Each stage appears immediately and repeats every ten seconds with elapsed seconds for that stage, so long builds do not leave the CLI silent. Repeated polling of the same deployment state does not reset its timer. Remote upload and image building share one stage because the control API completes both in the deployment request; these updates show elapsed time, not upload percentages or Docker build logs. Stdout still contains only the final summary or JSON result. `--no-wait` reports preparation progress and returns after control publishes desired state, without waiting for rollout.
36
+
37
+ The same progress reporting covers `rollback`, `backup run`, `restore run`, control-plane backup/restore, control setup, node add/replace/repair and node host teardown. Setup reports Docker installation, host checks, identity creation and installation; `--build-local-image` additionally reports image building, export, transfer and loading on the host. Node installation reports the wait for a fresh agent report. Interactive setup questions and confirmation finish before these stages start. Backups identify pending targets and their reported states, restores report their state changes, and rollback reports deployment states. The shared command wait used by `restart`, non-followed `logs` and node diagnostics in `doctor` also reports queued/running/terminal states. Heartbeats stop on completion, failure or timeout; existing timeouts and exit codes still apply. `--no-wait` skips the corresponding remote wait and its progress. `node enroll` only prepares enrollment instructions and does not wait for the host to enroll.
38
+
39
+ Both remote source uploads and `--build-local` use a temporary source snapshot of the current working directory. Tug applies the root `.dockerignore` using its supported pattern matcher and always excludes `.git`, `node_modules`, `data`, credential documents named `*.credential.json`, the Tug overlays and `.tug.json` at any depth, root Tug config names, and the resolved config, overlay and credential file paths. These mandatory exclusions cannot be re-included by ignore rules. The snapshot is validated before use and unsafe links are rejected. Docker then applies its own ignore rules to the local build. Tug does not detect arbitrary secret names or claim full Docker ignore compatibility: review your build contexts and Dockerfile-specific ignore rules, especially for nested or multiple contexts. A broad `COPY .` can still include secrets that Tug cannot identify.
40
+
41
+ ## Interactive selection
42
+
43
+ In a terminal outside CI, omit an existing target to fetch choices from control: `tug logs`, `tug exec`, `tug inspect`, `tug restart`, `tug deploy cancel`, `tug restore run`, `tug restore resume`, `tug restore release`, `tug secret remove`, `tug volume remove` and `tug credential revoke`. Node operations `replace`, `repair`, `disable`, `remove` and `ingress` also offer registered nodes when their name is omitted. Use `tug scale --replicas 3` or `tug node address --to 10.0.0.2` to supply the new value while choosing the target. `tug doctor --node` offers a node; plain `doctor` retains its existing behavior.
44
+
45
+ Menus show numbered choices with identifying details. Enter a number, or comma-separated numbers for multi-choice; enter `q` to cancel. Blank input never selects anything, even if there is only one choice. Menus and questions go to stderr, keeping JSON and command output on stdout usable. Without a terminal, or with `CI` set, missing targets fail with an explicit-target error and selection never reads stdin. Explicit targets skip their selection menu and discovery request.
46
+
47
+ Live `exec`, `run`, `inspect`, `logs --follow` and `ps` offer a node when placement is ambiguous and `--node` is omitted. The choices come from current desired placement, with node status shown alongside each name; stale or disabled placements remain visible and control still validates the operation. Pass both the service and `--node` to avoid discovery for live commands. To select a service while supplying a remote command, use `tug exec --select -- sh -c 'echo hello'` or `tug run --select -- wp core update-db`; with `--select`, all positional arguments belong to the remote command.
48
+
49
+ Commands whose omitted targets already mean “all” retain that behavior. Use these explicit selection options for a subset:
50
+
51
+ | Command | Choices |
52
+ | --- | --- |
53
+ | `tug backup run --select` | Enabled backup targets; cannot combine with explicit targets |
54
+ | `tug hold release --select` | Services held for a deployment move; excludes restore-owned holds |
55
+ | `tug node replace --select`, `tug node repair --select` | Registered nodes with SSH targets in local config; cannot combine with a node, SSH target or `--all`; continues and reports per-node failures |
56
+ | `tug restore run --select-snapshot` | Successful catalog backups for the chosen target, including explicitly labeled pre-restore safety backups; `--from` still accepts an external restic snapshot ID |
57
+ | `tug restore run --select-writers` | Services to stop instead of automatic writer discovery; cannot combine with writer flags or scratch mode |
58
+ | `tug credential create ci --role deploy --select-projects` | Existing projects, or “All projects, including future projects”; explicit repeatable `--project` also supports future project names |
59
+
60
+ Selections are project-scoped where the operation is project-scoped and honor the credential's permissions. Service choices use current desired state rather than potentially stale reports. Deployment cancellation lists active deployments without the status history limit; snapshot selection reads the successful backup catalog rather than the recent-run display. Restore recovery lists only interrupted restores, including those outside the recent history display. Volume removal offers only unreferenced volumes and identifies persistent data that requires `--delete-data`. A choice does not bypass control's validation or existing confirmation requirements: `restore run` still previews unless given `--confirm`, and volume/node removal retains its existing guards.
61
+
62
+ ## Config file
63
+
64
+ The CLI reads a YAML or JSON file, `tug.yml` by convention, so a repository can record how to reach its control plane and which servers it uses. [`examples/tug.yml`](../examples/tug.yml) is a complete sample.
65
+
66
+ - **Lookup.** `--config` or `TUG_CONFIG` select a file, which must exist. Otherwise the working directory is searched for `tug.yml`, `tug.yaml`, `tug.json` and `.tug.json`; more than one is an error, and none is fine. The format follows the extension (`.json` is JSON, anything else YAML, which also reads JSON). Only the working directory is searched, not its parents.
67
+ - **Overlay.** `tug.local.yml` next to `tug.yml` (`.tug.local.json` next to `.tug.json`, `<name>.local<ext>` in general) holds what must not be committed. It has the same keys plus `credential` (a path to a credential file), and its values win key by key, including inside `nodeDefaults` and each node. `credential` in the tracked file is an error. `tug init` writes the tracked file only when it is missing (an existing one keeps its comments), points the overlay at the imported credential file (written with mode `0600` under `~/.config/tug/credentials/`), and adds the overlay to `.gitignore` in a git checkout. Connection flags given to `init` for an existing tracked file are saved in the overlay.
68
+ - **Strict keys.** Unknown keys, wrong types and out-of-range values fail with the file and key in the message. Paths in the file (`file`, `ca`) resolve relative to the file, not the working directory.
69
+
70
+ Every key is a default for the flag it is named after, on every command that has that flag (a test enforces that the table below and the commands' real options agree), and a flag on the command line always wins. Each key is the camelCase of the long flag it supplies a default for, with the same value type. `nodeDefaults` keys that name `control setup` options (`installDocker`, `image`) are its defaults too. A repeatable flag takes a list (`label: [web, db]`; a single string is a one-item list), and a negated flag maps to the positive key set to `false` (`--no-wait` is `wait: false`). A flag on the command line always wins over the file; a list from the file is replaced by flags, not appended to. A key applies only to the commands listed for it.
71
+
72
+ | Key | Flag | Commands |
73
+ |---|---|---|
74
+ | `controlUrl` | `--control-url` (connections), the address `control setup` advertises | all; `control setup` also writes it into control's environment |
75
+ | `project` | `--project` | project-scoped commands |
76
+ | `credential` (overlay only) | `--credential`, `TUG_CREDENTIAL_FILE` | all |
77
+ | `file` | `-f, --file` | `deploy` |
78
+ | `buildLocal` | `--build-local` | `deploy` |
79
+ | `wait` | `--no-wait` | `deploy`, `rollback`, `backup run`, `node add`, `node replace`, `node repair` |
80
+ | `nodes.<name>.ssh` | `<ssh-target>` | `node add`, `node replace`, `node repair` |
81
+ | `nodes.<name>.address` | `--address` | `node add`, `node replace`, `node repair`, `node register`, `node enroll` |
82
+ | `label` | `--label` | `node add`, `node replace`, `node repair`, `node register`, `node enroll` |
83
+ | `installDocker` | `--install-docker` | `node add`, `node replace`, `node repair`, `node enroll`, `control setup` |
84
+ | `image` | `--image` | `node add`, `node replace`, `node repair`, `node enroll`, `control setup` |
85
+ | `agentControlUrl` | `--agent-control-url` | `node add`, `node replace`, `node repair`, `node enroll` |
86
+
87
+ `control setup` reads `controlUrl` from the selected file (or the discovered default file), with the overlay taking precedence. `controlUrl` is the single name for where control is. For `control setup` the address is, in order, `--control-url`, `TUG_CONTROL_URL`, the config file's `controlUrl`, then the SSH target's host and `--port`; the command prints which one it used. For every other command it is the connect target, falling back to the URL stored in your credential, so a tunnel is just `--control-url https://127.0.0.1:18082`. The credential's URL is never used as an installation address. A stale exported `TUG_CONTROL_URL` therefore changes what `control setup` advertises; unset it when installing. A configured URL includes its own public port; `--port` controls the host port published by Docker and does not rewrite the configured URL.
88
+
89
+ The rows from `label` down are node keys: set them under `nodes.<name>`, or once for every node under `nodeDefaults` (everything except `ssh` and `address`). Secrets and per-invocation choices have no key: `--value`, `--force`, `--confirm`, `--version`, `--ttl`, `--timeout`, `--build-local-image` and the like.
90
+
91
+ The file is not desired state and Tug never reconciles against it. Only `node add`, `node replace`, `node repair`, `node register` and `node enroll` read `nodes`, as defaults for flags; every other command asks the control plane. See [Nodes](nodes.md#declaring-nodes-in-the-config-file).
92
+
93
+ ## Setup
94
+
95
+ - `tug init [<credential-file>|-]` creates the config file with the resolved control URL and project when it is missing, imports the credential (a file, or `-` for stdin) into a `0600` file under `~/.config/tug/credentials/` and points the git-ignored overlay next to the config at it; see [Config file](#config-file). Use global `--control-url`, `--project` or `--config` to select them.
96
+ - `tug setup <ssh-target>` is a shortcut for `tug control setup <ssh-target> [--extra-san <name>]... [--install-docker] [--port <port>] [--image <tag>] [--build-local-image] [--credential-out <file>] [--no-import] [--recover-root] [-y]`. It installs control over SSH (the advertised address defaults to `controlUrl` from the config, then the SSH target's host and `--port`), issues the root credential and, like `init`, imports it into your local config, so no separate `init` step follows. In a terminal it offers to install Docker when it is missing, prints the plan and asks to confirm (`-y` skips the question); without a terminal it never prompts. `--credential-out <file>` also saves the credential to a `0600` file, and `--no-import` skips the import (it requires `--credential-out`). The mTLS port defaults to `8443`. `--recover-root` replaces only root's key from the control-plane host. See [Control plane](control-plane.md).
97
+ - `tug node address [node] [address] [--to <address>]` updates the registered private address used for published-port binding and cross-node routing.
98
+ - `tug node disable [node]` blocks a node's key immediately without SSH, deleting or changing placement or stopping its containers: it withdraws the node's service endpoints and ingress routes but does not drain workloads. `node replace` brings it back.
99
+ - `tug node add [<ssh-target>] [--name <node>] [--ingress] [-y] [--all] [--agent-control-url <url>] [--install-docker] [--address <ip>] [--label <label>]... [--image <tag>] [--build-local-image] [--no-wait]` creates a node and installs its agent over SSH. The name defaults to the first DNS label of the SSH host (`app1` for `root@app1.example.com`); with an IP address a terminal asks for the name, offering the host's own `hostname -s` as the default, and without a terminal `--name` is required. `node enroll` likewise asks for `--name` in a terminal (there is no host to suggest one). Without `<ssh-target>` the target is `nodes.<node>.ssh` from the [config file](#config-file). It checks for a control credential first, then always prints the plan (node, SSH target, Docker, address, image, ingress) to stderr before changing anything; without a terminal the plan says Docker was not checked. In a terminal it also checks for Docker and offers to install it, lists the host's global IPv4 addresses (private ones first, never defaulting to a public one) to pick the inter-node `--address` from, asks whether the node should handle ingress (`--ingress` answers yes) and asks to confirm (`-y` skips the question); without a terminal it never prompts, so scripts pass `--address`, `--ingress` and `--install-docker` explicitly. `--all` walks every node with an `ssh` entry, continues after failures and prints per-node results. Unless `--no-wait` is set the result carries `reporting`; a node that has not reported after about a minute prints `reporting: false` and the command exits nonzero. The agent uses host networking for TCP and HTTP health checks, without added capabilities. See [Nodes](nodes.md).
100
+ - `tug node replace|repair [<node>] [<ssh-target>] [--all] [same options]` runs the same SSH-assisted flow for an existing node: `replace` issues a new identity (it also brings back a disabled node) and `repair` first moves corrupt agent credential documents aside. `<node>` is positional; the target defaults to `nodes.<node>.ssh`.
101
+ - `tug node list [--json]` (alias `ls`) prints a table of the registered nodes with status, address, ingress role, labels and last contact, or the raw API result with `--json`.
102
+ - `tug node register <node> --csr <file> [--mode create|replace]` signs a node-generated CSR and prints the identity response (node, certificate, CA and public URL). It installs nothing; use `agent install-cert` on the host.
103
+ - `tug node enroll --name <node> [--address <addr>] [--agent-control-url <url>] [--install-docker] [--label <label>]... [--ttl <seconds>] [--image <tag>] [--json]` prepares a host to enroll itself: it mints a single-use, name-bound join key (an enrollment credential) and prints the Docker command that enrolls the host when run, or cloud-init with `--install-docker`. TTL defaults to `3600` seconds. A saved cloud-init document contains a credential. The printed image must be pullable by the new host; a development `tug:local` override must already be loaded there.
104
+ - `tug node remove [node] [sshTarget] [--force] [--teardown [--delete-data] [-y]]` removes a retired node from the control plane: its identity (the node ID is retired and never reused), statuses and queued commands, and any unconsumed join key. It is refused while a current deployment still places a service or volume on the node (deploy without it first) and while its agent is still reporting, unless `--force`. Stop the agent and remove its containers on the host yourself. With `--teardown` it also cleans the host over SSH after the control record is gone: it stops the agent and ingress, removes the Tug containers and networks and deletes `/etc/tug` and `/var/lib/tug`. Project volumes and ingress certificates are kept unless you add `--delete-data`. It asks for confirmation (`-y` skips it, and is required without a terminal), refuses a host that runs the control plane, and can be repeated if the cleanup failed after the record was removed. The SSH target defaults to `nodes.<name>.ssh` in the config file.
105
+ - `tug node ingress [node] [--disable]` enables the managed Caddy ingress role, or removes it with `--disable`.
106
+
107
+ Every control URL, including overrides, must be `https://`. Agents can dial out to control without an inbound control-channel port; cross-node workloads still need reachable node `address` values. See [Nodes](nodes.md) and [Networking](networking.md).
108
+
109
+ `--control-url` selects where the operator CLI connects and `--agent-control-url` where the agent connects; both are connect targets only, and the server is always verified by the control name derived from the pinned CA, so the address can be a DNS name, an IP, a tunnel or a moved host. Use them when the operator connects through a local SSH tunnel while nodes connect over a private network, for example `tug --control-url https://127.0.0.1:18082 node add root@app1.example.com --name app1 --agent-control-url https://10.110.0.3:8443`.
110
+
111
+ ## Secrets
112
+
113
+ `tug deploy` also resolves secrets for you. It scans the Compose file for variables with no default (`${NAME}` or `$NAME`) that are not a control secret, in the environment or in `.env`. In a terminal it asks, for each one, whether to generate a random 192-bit value or to enter one (hidden), and stores it before deploying; existing secrets are never overwritten and generated values are never printed. Without a terminal, or when `CI` is set, it fails immediately naming the variables, so a pipeline never hangs or deploys an empty value.
114
+
115
+ - `tug secret set <name> [--value <value>]` sets a secret, reading stdin if `--value` is omitted. After you enter the value and press Enter, it prints the result and exits without requiring EOF (Ctrl-D).
116
+ - `tug secret list` lists names and versions, without values.
117
+ - `tug secret remove [name]` deletes a secret.
118
+
119
+ ## Backups
120
+
121
+ The backup commands are `tug backup ...` and `tug restore ...`. These are workload backups of service commands and volumes; `control backup` below is the separate control-plane backup. Destination and restore commands need an admin credential.
122
+
123
+ - `tug backup destination set <url> [--env NAME ...]` sets where backups go, for example `s3:https://s3.example.com/bucket/tug-backups` with `--env AWS_ACCESS_KEY_ID --env AWS_SECRET_ACCESS_KEY`. Each value is read from the environment variable of that name, otherwise from a line on stdin; it is never a command-line argument. S3 needs both AWS variables. Values are stored encrypted on control and released only to a node running a backup. An absolute path is accepted for development, but it is read on each node and is not shared. Setting a destination replaces the previous one.
124
+ - `tug backup run [targets...] [--no-wait] [--timeout <seconds>]` backs up the named services or volumes now, or every backup target of the project, and waits for the result. It exits nonzero if any backup fails or is missed. Needs the `deploy` role.
125
+ - `tug backup list [--limit <n>] [--check]` shows each backup target with its node, schedule, last success and next run, then the most recent runs. With `--check` it also prints a `PROBLEM` line for each target that is stale (no success for two periods), whose last run failed or was missed, or when no destination is configured, and exits nonzero, so cron or a monitor can alert without parsing output. Needs the `read` role.
126
+ - `tug restore run [target] [--from <id|latest>] [--scratch] [--no-safety-backup] [--writers <service>...] [--no-writers] [--confirm]` restores a service or volume. Without `--confirm` it prints the plan and changes nothing. `--from` takes a backup run id, a restic snapshot id or `latest`. See [Restore](backups.md#restore). It waits until the services it stopped report serving again (up to five minutes, then it finishes with a warning). Needs an admin credential.
127
+ - `tug restore list` shows recent restores; `tug restore resume [id]` runs an interrupted one again; `tug restore release [id]` starts the services it stopped and unlocks the project.
128
+ - `tug backup destination show` prints the URL and the variable names, never values.
129
+ - `tug backup destination remove` removes the destination and its credentials. Existing backups stay in the repository.
130
+ - `tug backup password` prints the project's repository and its password. Each project has its own restic repository at `<destination>/<project>` with a password derived from control's master key, so keep the printed password with the destination credentials: losing the master key loses every repository.
131
+
132
+ These operations use the resolved project.
133
+
134
+ ## Deployments
135
+
136
+ - `tug deploy [-f, --file <file>] [--version <version>] [--build-local] [--no-wait] [--timeout <seconds>]` deploys the first of `compose.yaml`, `compose.yml`, `docker-compose.yaml` or `docker-compose.yml` found in the working directory by default. Services with `build:` upload the current directory for remote builds, or build on the operator's Docker daemon with `--build-local` and upload saved images without source. Published-image-only applications need no context upload. Control rejects a built service with no available build input. The display version defaults to `git-<Compose hash>`; deployment image identities are immutable even when that display version is reused. Local builds are single-platform and must match the target node architecture. Deployment waits for convergence by default, with a `300` second timeout; `--no-wait` returns once desired state is created. Needs Docker Compose v2 on the operator machine to normalize the file. With `--move <service>` (repeatable) it acknowledges that a service with data placed on another node starts empty there: without the flag such a deploy is refused. The services that write to the moved one are held until you restore it (`tug restore run`) or run `tug hold release`. If the wait is interrupted (control unreachable, timeout) the error names the deployment and `tug deploy cancel [id]`; a deploy refused because another is still rolling out names that one the same way. It prints one summary line (`site git-ab12: deployment dep_1 succeeded, 3 services`); `--json` prints the full result with the desired state. `rollback` and `scale` print a summary the same way and also take `--json`. `deploy` warns when the file's `name:` differs from the Tug project, because secrets, backups and restores are scoped to the project.
137
+ - `tug status [--json]` summarizes each project’s current deployment, node reports and errors, plus node status and last contact. Stale node health is identified as the last report. `--json` prints the raw API result (all projects, recent deployments, nodes and per-node status) for scripts. This is still a control-wide, credential-scoped view.
138
+ - `tug events [--limit <n>] [--all]` lists events in the resolved project, or in every project with `--all` (including control events that belong to no project, such as credential changes); the limit defaults to `100`, with a maximum of `500`. Control actions show the `actor` credential name that caused them. Agents add a `service.*` event when a service changes, not on every poll: `pulling`, `starting`, `health_checking` and `serving` when a new revision, changed secrets or a changed rendered configuration are applied, one `failed` event when an apply fails, and one `serving` event when it recovers. Control deletes events older than 30 days and keeps at most 100,000; see [Configuration](configuration.md#control-plane).
139
+ - `tug rollback [--force] [--no-wait] [--timeout <seconds>]` rolls the resolved project back; `--force` permits rollback while a deployment is active. Waiting defaults to `300` seconds. Projects with `x-tug: { rollback: auto }` roll back failed deployments themselves; see [Compose extensions](compose.md#automatic-rollback).
140
+ - `tug deploy cancel <deployment-id>` cancels an active deployment.
141
+
142
+ To build on a laptop and upload image archives, run `tug deploy --build-local -f docker-compose.yml`. To deploy CI-built images, push them to your registry and declare `image:`; control pins tags to digests before publishing the deployment. See [Private registry credentials](control-plane.md#published-images-and-private-registries).
143
+
144
+ Local builds use generated temporary image tags and remove those tags after saving each archive, including cleanup attempts after build/save failure. They do not remove your published image tags or the Tug runtime image. A refused cleanup produces a warning without hiding the original build error.
145
+
146
+ ## Operator credentials
147
+
148
+ Scoped credentials for CI and teammates; see [Access control](access-control.md) for roles, scoping and a GitHub Actions example. All three commands need an admin credential.
149
+
150
+ - `tug credential create <name> --role read|deploy|admin [--project <name> ...] [--expires-in-days <n>]` creates an operator credential and prints it once on stdout; control keeps no copy of its private key. Repeat `--project` to allow several projects; without it the credential covers all projects (admin credentials cannot be limited). Here `--project` selects the credential's scope rather than the connection project. Names are 1 to 64 letters, numbers, dots, underscores or hyphens, and cannot be reused after revocation. Expiry is at most `365` days and defaults to `365`.
151
+ - `tug credential list` lists operator credentials with role, projects, creator, last use, expiry and revocation, never secrets.
152
+ - `tug credential revoke [name]` revokes a credential immediately and closes its live sessions.
153
+
154
+ Use a created credential anywhere a credential works, for example `TUG_CREDENTIAL=<credential> tug deploy`.
155
+
156
+ ## Services and volumes
157
+
158
+ - `tug restart [service] [--timeout <seconds>]` restarts a service and waits for the result; timeout defaults to `60` seconds.
159
+ - `tug hold release [service...]` starts the services that `tug deploy --move` held, without restoring them (all of them when none is named). A restore releases its own holds.
160
+ - `tug image prune [--keep <n>] [--dry-run]` (admin) deletes build archives kept on control that rollback no longer needs: all but the newest `n` deployments of each project (default 5), the current deployment and any version a node still reports. Rolling back to a pruned deployment is refused; deploy again instead. `tug control backup --no-images` leaves the archives out of a control backup.
161
+ - `tug scale [service] [replicas] [--replicas <n>]` changes how many matching nodes run a service, spreading by count over nodes that carry the service's `placement.labels` (all nodes without labels), staying within an explicit `placement.nodes` when the service has no labels, and updates the current desired state without a new deployment. `global` services run on every matching node and are scaled to that set. The next `deploy` re-resolves placement from the Compose file.
162
+ - `tug exec [service] [-i, --interactive] [-t, --tty] [--node <name>] [-- <command> ...]` executes a command in the current service container, defaulting to `sh`. Arguments are passed directly without a shell. `-i` forwards stdin. `-t` allocates a terminal (with resize) and requires a local terminal. Use `--node` when a service has more than one placement.
163
+ - `tug run [service] [--node <name>] -- <command> ...` runs a one-off command in a new container instead of the running one: the service's image, environment (secrets included), user, working directory, network and volume mounts, no published ports, removed when it ends. It is for jobs that should not run inside the live container, such as `tug run web -- wp core update-db`. It is non-interactive, exits with the command's exit code, needs the `deploy` role and is recorded as a `job.run` event with its command line. Mounts are shared with the service, so a destructive command is the operator's responsibility; the one-off container does not take the project lock.
164
+ - `tug logs [service] [--tail <n>] [-f, --follow] [--node <name>]` fetches recent service logs through the durable command queue, or follows them live with `-f`; the default is `200` lines. `--node` applies to followed logs and is required when the service has more than one eligible node.
165
+ - `tug ps [--node <name>]` prints live per-container state as JSON. Without `--node`, the project must resolve to exactly one eligible connected node.
166
+ - `tug inspect [service] [--node <name>]` prints the live Docker inspection object as JSON with environment variable values redacted (names are kept). It requires the `deploy` role, which grants access to the workload's secrets regardless. Use `--node` when a service has more than one placement.
167
+ - `tug volume list` lists recorded volumes.
168
+ - `tug volume remove [name] [--delete-data]` removes a volume record; `--delete-data` also removes the Docker volume on its node. See [Compose extensions](compose.md#volumes).
169
+
170
+ These operations use the resolved project. Live operations use an authenticated WebSocket from the CLI through control to the agent's existing outbound connection. They are ephemeral: control does not queue or replay them, and disconnecting any hop closes the local session. Cancelling or disconnecting `exec` kills the agent's Docker client, but an already-started process inside the container may continue because the Docker exec stream does not expose a remote process identifier.
171
+
172
+ ## Operations
173
+
174
+ - `tug doctor [--nodes] [--node [name]] [--timeout <seconds>]` runs diagnostics on this machine (Node.js, Docker and the Compose plugin that `deploy` needs) and on the control plane: SQLite, the CA and server certificate expiry, each node's certificate expiry and connectivity, each online node's observed clock skew (unknown for offline nodes; see [Nodes](nodes.md#doctor-certificate-expiry-and-clock-skew)), and ingress status. `--nodes` also runs host diagnostics through the agent on every node and `--node` on one; both wait for the agent result (timeout defaults to `60` seconds) instead of returning the queued command id. An unknown `--node` is an error. Node diagnostics include `ports.public`, a warning listing containers of Tug projects that listen on all interfaces or a public IP ([Networking](networking.md#published-ports-private-by-default)).
175
+ - `tug control backup` creates a control-plane backup: control's state, PKI, master key and retained built-image archives. It does not capture workload volume contents; those are covered by [workload backups](backups.md).
176
+ - `tug control restore <artifact-path> --confirm` restores a control-plane backup and requires a control restart. The archive path is on the control-plane host. See [Control plane](control-plane.md#backup-and-restore).
177
+
178
+ ## CLI architecture and naming
179
+
180
+ The interface follows the relevant conventions used by `wp-release`: `tug <subject> <verb>` operations, with the everyday project commands at the top level; kebab-case command/flag names; primary targets as positional arguments; named qualifiers with consistent meanings; and positive camelCase internal options (`--no-wait` becomes `wait: false`, never `noWait`). New structured CLI results use camelCase; API responses printed unchanged may retain their existing database field names. Help, docs and tests must change together when the interface changes.
181
+
182
+ CLI adapters resolve configuration, parse arguments and format results. Build, archive, installation and API primitives receive explicit values and injectable runners; they do not discover CLI config or read unrelated environment variables. Keep domain logic in ordinary module functions, and introduce stateful classes only when a multi-step workflow needs shared state/dependencies. Only operations that use Docker or SSH should require those tools. Validation must complete before side effects, and stdout intended as a runnable artifact or JSON must remain directly usable. These conventions do not imply GitHub Actions logging or additional output modes.
@@ -0,0 +1,245 @@
1
+ # Compose extensions
2
+
3
+ Tug deploys standard Docker Compose files with an `x-tug` extension on services and volumes for placement and health checks. Compose is used only on the operator machine, where `tug deploy` normalizes the file; nodes run no Compose. Each agent turns the services placed on its node into Docker Engine API container definitions, without `x-tug`, `build`, `deploy` and `container_name`, with `extra_hosts` added for services on other nodes and `image` set to the built or declared image. Published `ports` without a host IP are bound to the node's `address` or `127.0.0.1` instead of every interface; write the host IP (for example `"0.0.0.0:80:80"`) to publish on purpose. See [Published ports](networking.md#published-ports-private-by-default).
4
+
5
+ See [Health checks](healthchecks.md) to choose a probe and [Service placement and scaling](service-placement.md) to choose where services run.
6
+
7
+ ## Services
8
+
9
+ ```yaml
10
+ services:
11
+ web:
12
+ image: nginx:1.27-alpine
13
+ ports:
14
+ - "8088:80"
15
+ environment:
16
+ SAMPLE_SECRET: ${SAMPLE_SECRET:-not-set}
17
+ x-tug:
18
+ placement:
19
+ nodes: [vps-a]
20
+ health:
21
+ type: http
22
+ port: 80
23
+ path: /
24
+ ```
25
+
26
+ - `placement.nodes`: node names that run the service. With one registered node, a service without placement goes to that node. With multiple nodes, choose explicit nodes, labels or `global: true` for every service.
27
+ - `placement.labels`: node labels (tags) that select the nodes running the service. Nodes must carry all listed labels. Combined with `replicas: N`, the control plane spreads the service across N matching nodes; `global: true` runs one replica on every matching node. Nodes whose agent has not been seen within `TUG_STALE_SECONDS` are skipped when labels or `global` are resolved (at deploy and on `tug scale`), so a dead host cannot hold a rollout open. Explicit `placement.nodes` still pins a service to named nodes, and `tug scale` never moves a pinned service off them.
28
+ - `global`: run one replica on every node matching `placement.labels` (or every node when no labels are given).
29
+ - `replicas`: optional. With explicit nodes and no labels, it must equal the number of named nodes. With label placement, it selects up to that many matching nodes. Tug runs at most one replica of a service per node.
30
+ - `ingress`: publishes the service on a hostname through the managed Caddy edge (see [Networking](networking.md#layer-b-ingress-and-tls)). `host` is a fully qualified hostname, unique across the file; `port` is the service's container port or published host port and must appear in `ports`; `tls` is `auto` (default, ACME), `custom` or `off`. `tls: custom` also needs `cert` and `key`, the names of project secrets holding the PEM certificate chain and private key, which must exist before you deploy.
31
+ - `rollout`: `strategy: recreate` (default) or `rolling`, plus `drain`; see [Rolling rollouts](#rolling-rollouts). The top-level `x-tug: { rollback: auto }` is described under [Automatic rollback](#automatic-rollback).
32
+ - `health`: checked by the agent after the container starts; the service is `serving` only once it passes.
33
+ - `type: docker` waits for the container to be running and, if it defines a Docker `HEALTHCHECK`, healthy.
34
+ - `type: tcp` connects to `port` on the container IP.
35
+ - `type: http` requests `http://<container-ip>:<port><path>` (defaults `80` and `/`) and expects a 2xx response after following redirects.
36
+ - `interval` (default `2s`), `timeout` (default `2s`) and `retries` (default `10`) accept numbers in milliseconds or `ms`/`s`/`m` suffixes.
37
+
38
+ `depends_on` sets start order on a node. `condition: service_healthy` waits on the dependency's `x-tug.health` when it has one; otherwise it checks that the container is running and, if present, its Docker `HEALTHCHECK` is healthy. Without either health check it only verifies that the container is running. A failed apply is included in the deployment error `tug deploy` prints, and recorded once as `service.failed`.
39
+
40
+ ```yaml
41
+ x-tug:
42
+ placement:
43
+ nodes: [app1]
44
+ ingress:
45
+ host: app.example.com
46
+ port: 80
47
+ tls: auto # auto | custom | off
48
+ ```
49
+
50
+ Services that run on a different node than a given service are reachable from it by name (`db` or `db.<project>.tug`) by default, as long as the target publishes its port and the client's node can route to the target node's private `address`. Tug binds the port to that address; the operator's firewall controls access to it. Set `TUG_SERVICE_NAMES=false` on control to disable the injected names. The agent adds these names to the containers as hosts-file (`extra_hosts`) entries, not DNS records; see [Networking](networking.md#layer-a-cross-node-service-names).
51
+
52
+ Services with `build:` are built by control from uploaded source, or on the operator machine with `deploy --build-local`. Each build receives an immutable tag distinct from the display version and is delivered as a saved image archive from control. Build services require a source context or local image upload. Published `image:` tags are resolved once on control to `image@sha256:…` before deployment publication; explicit SHA-256 references remain pinned. Multi-platform indexes remain intact. Agents pull the stored digest from the upstream registry, so rollback and nodes added later use that same manifest. Private registry credentials use the encrypted project secret described in [Control plane](control-plane.md#published-images-and-private-registries). Uploaded source contexts must contain regular files/directories within the build root; symlinks and escaping build paths are rejected.
53
+
54
+ ### Rolling rollouts
55
+
56
+ By default a changed service is recreated in place, which drops its traffic for a moment. A service behind the managed ingress can instead opt in to a rolling rollout:
57
+
58
+ ```yaml
59
+ services:
60
+ web:
61
+ image: ghcr.io/acme/web:2
62
+ ports:
63
+ - "8080:80"
64
+ x-tug:
65
+ placement:
66
+ nodes: [app1]
67
+ health:
68
+ type: http
69
+ port: 80
70
+ ingress:
71
+ host: app.example.com
72
+ port: 80
73
+ rollout:
74
+ strategy: rolling # recreate (default) | rolling
75
+ drain: 30s # how long the previous revision keeps running after takeover (default 30s)
76
+ ```
77
+
78
+ With `strategy: rolling` the agent starts the new revision next to the old one, health-checks it, and only then lets the ingress switch to it. The old revision is removed once `drain` has passed. If the new revision never becomes healthy it is removed, the old one keeps serving and the node reports `failed` for that deployment (see [automatic rollback](#automatic-rollback)). Nothing else on the node restarts: a rolling service whose rendered definition and secrets did not change is left alone, and so are recreated services.
79
+
80
+ `validateCompose` requires a rolling service to have `x-tug.ingress` (the edge is what switches traffic), `x-tug.health` (a revision only takes over once healthy) and exactly one entry in `ports` (the ingress port). `drain` takes the same durations as health checks (`500ms`, `30s`, `1m`, or a number of milliseconds) and should be longer than the poll intervals of the app node and the ingress node together (5s each by default), because the ingress only learns about the new revision through the app node's next status report and the edge node's next poll. `maxSurge` and per-service batch sizes are not supported: one new revision is started per changed service.
81
+
82
+ How it works, and its limits:
83
+
84
+ - Rolling services are meant to be stateless. Each revision is its own container (`tug-<project>-<service>-<hash8>`), and old and new revisions run side by side, so named volumes are shared between them (the project's `tug_v<project-length>_<project>_<volume>`) and concurrent access is the operator's responsibility. Use `recreate` for databases and anything that cannot have two writers.
85
+ - Each revision publishes the ingress port on its own host port from a free range (`TUG_ROLLOUT_PORTS` on the agent, default `20000-20999`), not on the declared host port. The declared port, for example `8080` above, is only the contract for the ingress validation. Reach a rolling service through its ingress hostname; see [Networking](networking.md#rolling-services-and-service-names) for what that means for service names.
86
+ - `depends_on` is dropped for rolling services and from recreated services that point at them, because rolling revisions are started and replaced on their own schedule rather than as part of the project's start order. All revisions share the project network, so names such as `db` still resolve; use health checks and client retries rather than start order.
87
+ - Switching a service from `recreate` to `rolling` keeps the old container in the main project serving until its replacement takes over and then removes it after the drain; switching back keeps the rolling revision for one drain after the recreated container is up. A service that had no container yet starts on a range port straight away.
88
+ - A rejected revision is not retried on every poll; deploy again (or roll back) to try again.
89
+
90
+ ### Automatic rollback
91
+
92
+ Add a top-level `x-tug` block to roll back failed deployments on its own:
93
+
94
+ ```yaml
95
+ x-tug:
96
+ rollback: auto # manual (default) | auto
97
+
98
+ services:
99
+ # ...
100
+ ```
101
+
102
+ When a deployment turns `failed` because an assigned node reported failed health for it, and the project has an earlier `succeeded` deployment, control creates a rollback deployment through the same path as `tug rollback` and records a `deployment.auto_rollback` event (`tug events` shows it). Both the rollback deployment and the event carry the actor `system:auto-rollback` (see [Access control](access-control.md#audit-trail)). If it cannot, it records `deployment.auto_rollback_skipped` with the reason (no earlier succeeded deployment, the project is locked, another deployment replaced the failed one). A failing rollback deployment is never rolled back again, so a bad rollback target cannot loop; for the same reason `rollback-` is reserved at the start of deployment versions and `deploy` rejects it. With rolling services the previous revision is still serving when the new one fails, so the rollback is only a state change and users see no downtime.
103
+
104
+ ## Volumes
105
+
106
+ ```yaml
107
+ volumes:
108
+ cache-data:
109
+ x-tug:
110
+ type: local
111
+ persistent: true
112
+ placement:
113
+ nodes: [vps-c]
114
+ ```
115
+
116
+ Control records volumes per project. A volume is included in a node's Compose file only when a service on that node uses it and, if the volume has `placement.nodes`, the node is listed. `tug volume remove` refuses to remove a persistent volume without `--delete-data`, and refuses any volume still referenced by a service in the current deployment. `--delete-data` queues a `docker volume rm` on the volume's placement node.
117
+
118
+ Before starting workloads, the agent verifies that an existing Tug project network belongs to the project and that each existing non-external volume has the expected project and volume labels, driver and options. A mismatch stops reconciliation with an error instead of silently reusing the resource. Volume deletion also checks those ownership labels. Tug reserves the `tug.project`, `tug.volume` and `tug.restore` labels on managed volumes. External volumes are intentionally selected by name and are not checked as Tug-owned resources.
119
+
120
+ Tug does not manage network file systems. To share a directory between nodes, mount it with the plain Compose `driver: local` and `driver_opts` against a server you run, such as a NAS, a managed export or your own NFS host, and install the NFS client (`nfs-common` on Debian and Ubuntu) on the nodes that use it:
121
+
122
+ ```yaml
123
+ volumes:
124
+ uploads:
125
+ driver: local
126
+ driver_opts:
127
+ type: nfs
128
+ o: addr=10.0.0.10,rw,hard
129
+ device: ":/srv/uploads"
130
+ ```
131
+
132
+ Every node that uses the volume mounts it itself, so the file server is a single point of failure that you operate. Prefer S3-compatible object storage for uploads when the application supports it.
133
+
134
+ ## Backups
135
+
136
+ ```yaml
137
+ x-tug:
138
+ backup:
139
+ schedule: "0 3 * * *"
140
+ retention: { daily: 7, weekly: 4, monthly: 6 }
141
+
142
+ services:
143
+ db:
144
+ image: mariadb:11.4
145
+ x-tug:
146
+ placement: { nodes: [app1] }
147
+ backup:
148
+ command: mariadb-dump -u root -p"$$MARIADB_ROOT_PASSWORD" --single-transaction --databases wordpress
149
+ filename: wordpress.sql
150
+ verify: "-- Dump completed"
151
+
152
+ volumes:
153
+ uploads:
154
+ x-tug:
155
+ placement: { nodes: [app1] }
156
+ backup: "30 3 * * *"
157
+ ```
158
+
159
+ `x-tug.backup` marks something to back up. Tug does not know what your services are, so there are only two kinds of target:
160
+
161
+ - **A service**: Tug runs your `command` inside the running service container and stores its standard output. The container already has the right client, credentials in its environment and network access, so a database dump, `pg_dump`, `redis-cli --rdb -`, `tar` of a directory or `wp db export -` are all just commands. A string runs through `sh -c`; a list runs as written.
162
+ - **A top-level volume**: Tug mounts the volume read-only and archives its files.
163
+
164
+ The value is a cron string, `true` (a volume, with everything from the project defaults), `false`, or an object. The top-level `x-tug.backup` only holds defaults and enables nothing by itself.
165
+
166
+ | Key | Applies to | Meaning | Default |
167
+ | --- | --- | --- | --- |
168
+ | `schedule` | both | Five-field cron in UTC, or `@hourly`, `@daily`, `@weekly`, `@monthly`, `@yearly` | the project default; an error if neither is set |
169
+ | `retention` | both | restic keep policy with `last`, `hourly`, `daily`, `weekly`, `monthly`, `yearly` | the project default, else `daily: 7, weekly: 4, monthly: 6` |
170
+ | `jitter` | both | Seconds of delay after the scheduled minute, 0 to 3600 | a stable per-target value under 15 minutes |
171
+ | `timeout` | both | Hard limit for a run, `1m` to `24h` | `2h` |
172
+ | `memory` | both | Memory limit of the restic helper container, `128m` to `16g` | `512m` |
173
+ | `writers` | both | Services to hold during a restore | discovered from `depends_on`, host names in environment variables and volume mounts |
174
+ | `command` | service | What to run in the container; its output is the backup | required |
175
+ | `restore` | service | What to run in the container to load a backup back; the stored file arrives on its standard input | none (the service cannot be restored by Tug) |
176
+ | `filename` | service | Name of the file inside the snapshot | `<service>.out` |
177
+ | `verify` | service | Text that must appear in the last 4 KiB of output | none |
178
+ | `exclude` | volume | restic exclude patterns | none |
179
+
180
+ A run fails, and no snapshot is kept, when the command exits non-zero, writes nothing, or its output lacks the `verify` text (use the line your tool prints when it finishes, such as `-- Dump completed`, so a dump cut short is never mistaken for a good one).
181
+
182
+ Deploy rejects a service target that is global, replicated or placed on several nodes, and a volume used on several nodes without `x-tug.placement.nodes`. Tug does not check that a volume backup and a command backup overlap or that a volume holds a live database: copying the files of a running database is rarely a consistent backup, so back databases up with a command. Recipes for common databases are in [Backups](backups.md#recipes).
183
+
184
+ ## Secrets in services
185
+
186
+ Secrets are referenced by variable name and resolved by control at deploy time; the `@@tug:secret:NAME@@` placeholder you may see in a normalized file is internal and never written by hand. Reference secrets with `${NAME}` anywhere Compose interpolates, usually `environment`. `tug deploy` sets every project secret as a variable during `docker compose config`, so a secret wins over a local variable of the same name and `environment: [DB_PASSWORD]` passes it through. Only secrets a service references are released to its node, and a changed secret recreates exactly the services that use it. Secrets are substituted into the container environment; keep them out of `command` unless you accept them showing in `docker inspect`. Deploying a Compose file that references a secret the project does not have is rejected.
187
+
188
+ ## A database on the same node
189
+
190
+ An app reaches a database by Compose's own DNS only when they are on the same node. A name injected for a service on another node points at that node's address, and the client has to use the published host port, so an unpublished database is not reachable across nodes. Local volumes are not copied between nodes; label placement can spread replicas across matching nodes.
191
+
192
+ ```yaml
193
+ services:
194
+ db:
195
+ image: mariadb:11.4
196
+ command: ["--innodb-buffer-pool-size=48M"]
197
+ environment:
198
+ MARIADB_DATABASE: app
199
+ MARIADB_USER: app
200
+ MARIADB_PASSWORD: ${DB_PASSWORD}
201
+ MARIADB_ROOT_PASSWORD: ${DB_ROOT_PASSWORD}
202
+ volumes:
203
+ - db-data:/var/lib/mysql
204
+ x-tug:
205
+ placement:
206
+ nodes: [app]
207
+ health:
208
+ type: tcp
209
+ port: 3306
210
+
211
+ web:
212
+ image: nginx:1.27-alpine
213
+ depends_on: [db]
214
+ ports:
215
+ - "8080:80"
216
+ x-tug:
217
+ placement:
218
+ nodes: [app]
219
+ health:
220
+ type: http
221
+ port: 80
222
+ ingress:
223
+ host: app.example.com
224
+ port: 80
225
+ tls: auto
226
+
227
+ volumes:
228
+ db-data:
229
+ x-tug:
230
+ type: local
231
+ persistent: true
232
+ placement:
233
+ nodes: [app]
234
+ ```
235
+
236
+ Set `DB_PASSWORD` and `DB_ROOT_PASSWORD` with `tug secret set` before deploying. The MariaDB flag keeps the buffer pool small enough for a 1 GB host that is also running the agent. A `service_healthy` dependency can use the database service’s `x-tug.health`; without a Tug or Docker health check it verifies only that the container is running.
237
+
238
+ ## Not supported
239
+
240
+ - `container_name` (Tug names containers `tug-<project>-<service>-<hash8>` so projects stay isolated).
241
+ - `deploy` (Swarm semantics).
242
+
243
+ ## Supported keys
244
+
245
+ `tug deploy` normalizes the file with `docker compose config` (Docker Compose v2 must be installed on the operator machine), and control accepts only these service keys: `image`, `build`, `command`, `entrypoint`, `environment`, `ports`, `expose`, `volumes` (named volumes and absolute bind mounts), `tmpfs`, `healthcheck`, `restart`, `labels`, `depends_on` (`service_started`, `service_healthy`), `extra_hosts`, `user`, `working_dir`, `hostname`, `init`, `read_only`, `shm_size`, `mem_limit`, `mem_reservation`, `cpus`, `pids_limit`, `cap_add`, `cap_drop`, `ulimits`, `logging`, `stop_grace_period`, `stop_signal` and `x-tug`. `pull_policy` is ignored because Tug decides when images change. Anything else is rejected with its key named, including `network_mode`, `privileged`, `devices`, `pid`, `ipc`, `container_name`, `env_file` (already merged by Compose), custom networks, and top-level `configs` and `secrets`. Top-level volumes accept `driver`, `driver_opts`, `labels`, `external`, `name` and `x-tug`. Containers use Tug's own network, volume and container names (`tug_<project>`, `tug_v<project-length>_<project>_<volume>`), so existing Compose-created volumes are not reused. If a matching owned volume exists under an earlier Tug name and its new-name volume does not, Tug refuses to create an empty replacement or restart the workload. The older volume remains on the node; recover or copy its data before deploying.
@@ -0,0 +1,52 @@
1
+ # Configuration
2
+
3
+ ## CLI
4
+
5
+ CLI options override environment variables, which override the config files and documented fallback. The config is a YAML or JSON file that is safe to commit: it holds the control URL, default project, defaults for a few command options and the servers you install. The operator credential is never read from it. See [Config file](cli.md#config-file) for the keys and how they map to flags, and [Nodes](nodes.md#declaring-nodes-in-the-config-file) for the `nodes` section.
6
+
7
+ | Variable | Purpose |
8
+ |---|---|
9
+ | `TUG_CONFIG` | Path to the CLI config file (default: `tug.yml`, `tug.yaml`, `tug.json` or `.tug.json` in the current directory). |
10
+ | `TUG_CONTROL_URL` | Connect target for the control API (an `https://` URL; for example a tunnel). The server is verified by its generated control name regardless; overrides the config file and the credential's default address. |
11
+ | `TUG_PROJECT` | Default project; overrides the config file. |
12
+ | `TUG_CREDENTIAL` | Encoded operator credential (for CI). Conflicts with `TUG_CREDENTIAL_FILE`; overrides the overlay. |
13
+ | `TUG_CREDENTIAL_FILE` | Path of a credential file; overrides the `credential` path in the untracked overlay. |
14
+
15
+ ## Control plane
16
+
17
+ | Variable | Default | Purpose |
18
+ |---|---|---|
19
+ | `PORT` | `8443` | The single mTLS port. |
20
+ | `TUG_CONTROL_URL` | required | Credential-free `https://` origin clients connect to by default, including the port. It is written into credentials and its host is added to the server certificate, but clients verify the generated control name, not this address. |
21
+ | `TUG_EXTRA_SANS` | unset | Comma-separated extra DNS names or IPs for the server certificate. |
22
+ | `TUG_RENEW_MIN_INTERVAL_MS` | `3600000` | Minimum time between certificate renewals per node. |
23
+ | `TUG_DATA_DIR` | `./data` | SQLite database, master key, `pki/` (CA and server certificate), image archives, build contexts and backups. |
24
+ | `TUG_UPLOAD_DIR` | `<data dir>/uploads` | Temporary storage for uploaded source contexts and local image archives. |
25
+ | `TUG_MAX_IMAGE_BYTES` | `5368709120` | Maximum bytes per uploaded file (source context or compressed image); expanded archives are limited to 20 GiB and 100,000 entries. |
26
+ | `TUG_LOCK_TTL_MS` | `600000` | Lease lifetime of per-project operation locks; active builds renew their lease. |
27
+ | `TUG_STALE_SECONDS` | `60` | Seconds without an authenticated work fetch, status report or wake wait before a node is marked `stale`. |
28
+ | `TUG_EVENT_RETENTION_DAYS` | `30` | Events older than this many days are deleted; `0` keeps events regardless of age. Pruning runs at startup and then at most once an hour while agents report events. |
29
+ | `TUG_EVENT_MAX_ROWS` | `100000` | Only this many of the newest events are kept; `0` removes the limit. A value that is not a whole number falls back to the default. |
30
+ | `TUG_CONTROL_BACKUP_DIR` | `<data dir>/control-backups` | Control backup archives. |
31
+ | `TUG_CONTROL_RESTORE_DIR` | `<data dir>/control-restore` | Scratch directory for restores. |
32
+ | `TUG_SERVICE_NAMES` | `true` | Publish health-aware `serviceEndpoints` so agents inject cross-node `extra_hosts` using each node's `address`. Set `false` to opt out. See [Networking](networking.md#layer-a-cross-node-service-names). |
33
+ | `TUG_ACME_EMAIL` | unset | Email for ACME registration in rendered Caddy configs. |
34
+
35
+ ## Agent
36
+
37
+ | Variable | Default | Purpose |
38
+ |---|---|---|
39
+ | `TUG_CONTROL_URL` | unset | Connect-only override of the control address (for example a private endpoint); the server is verified by its generated control name. |
40
+ | `TUG_JOIN_KEY` / `TUG_JOIN_KEY_FILE` | unset | Join key (or a `0600` file holding it) used only when the node has no identity yet; ignored once `agent.json` exists. |
41
+ | `TUG_NODE_NAME` | hostname | Node name when not bound by the enrollment key. |
42
+ | `TUG_NODE_ADDRESS` | unset | Private inter-node address reported at enrollment; also used for service endpoints, ingress upstreams and published-port binding. |
43
+ | `TUG_STATE_DIR` | `/var/lib/tug` | Identity (`agent.json`, `agent-pending.json`), applied revisions, managed-image identities, temporary image downloads, ingress config and generated project files. |
44
+ | `TUG_POLL_INTERVAL_MS` | `5000` | Maximum quiet interval between reconciliations. A WebSocket or long-poll wake can start one sooner. |
45
+ | `TUG_APPLY` | `true` | Set `false` to render files and report without touching Docker. |
46
+ | `TUG_ROLLOUT_PORTS` | `20000-20999` | Host port range (`LOW-HIGH`) for revisions of rolling services; each running or draining revision uses one port. |
47
+ | `TUG_CADDY_IMAGE` | `caddy:2-alpine` | Image for the managed Caddy container on ingress nodes. |
48
+ | `NODE_EXTRA_CA_CERTS` | unset | Extra CA for a control plane with a private certificate. |
49
+
50
+ ## Entrypoint
51
+
52
+ `TUG_ROLE=control|agent` selects the role when `src/main.ts` (`dist/main.js` in the image, whose entrypoint is `node dist/main.js`) is started without an argument. An argument always wins over the variable. The runtime image has no default command, so `docker run -e TUG_ROLE=agent tug` starts the agent and a bare `docker run tug` prints the usage message; the installers and the compose files pass the role as an argument.