odysseus-cli 0.2.0 → 0.9.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +332 -0
- data/LICENSE.txt +26 -0
- data/README.md +472 -88
- data/bin/odysseus +180 -122
- data/lib/odysseus/cli/cli.rb +448 -821
- data/lib/odysseus/cli/doctor_commands.rb +93 -0
- data/lib/odysseus/cli/interactive_commands.rb +198 -0
- data/lib/odysseus/cli/rollback_commands.rb +107 -0
- data/lib/odysseus/cli/setup_commands.rb +176 -0
- data/lib/odysseus/cli/ui.rb +457 -0
- data/lib/odysseus/cli/version.rb +7 -0
- metadata +22 -53
- data/lib/odysseus/cli/gum.rb +0 -156
data/README.md
CHANGED
|
@@ -58,16 +58,63 @@ ssh:
|
|
|
58
58
|
2. Build and deploy:
|
|
59
59
|
|
|
60
60
|
```bash
|
|
61
|
-
#
|
|
62
|
-
odysseus deploy --
|
|
61
|
+
# From a git repository with everything committed — the version is the commit
|
|
62
|
+
odysseus deploy --build
|
|
63
|
+
|
|
64
|
+
# Anywhere else, name the version yourself
|
|
65
|
+
odysseus deploy --build --image v1.0.0
|
|
63
66
|
```
|
|
64
67
|
|
|
68
|
+
Both work. They are not equivalent — see [Naming the
|
|
69
|
+
version](#naming-the-version) for what the second one costs you.
|
|
70
|
+
|
|
71
|
+
### Naming the version
|
|
72
|
+
|
|
73
|
+
Every deploy is tagged with a version, and there are two ways to get one.
|
|
74
|
+
|
|
75
|
+
**From a git commit — the proper way.** Run `deploy` in a git repository with
|
|
76
|
+
nothing uncommitted and omit `--image`. Odysseus tags the image with the commit
|
|
77
|
+
SHA and **refuses to deploy while the working tree is dirty**, which is the
|
|
78
|
+
point: the tag then provably identifies the code that is running. It records the
|
|
79
|
+
commit and your identity in each host's deploy log, so `odysseus rollback --list`
|
|
80
|
+
can tell you which commit a version was and who shipped it.
|
|
81
|
+
|
|
82
|
+
**With `--image TAG` — quick and dirty.** No git repository is needed, so it
|
|
83
|
+
works anywhere, and it is the right tool for trying something out or deploying
|
|
84
|
+
an image you built elsewhere. What you give up:
|
|
85
|
+
|
|
86
|
+
- **No commit is recorded.** `rollback --list` shows the version but cannot tell
|
|
87
|
+
you what code it was, because an arbitrary tag has no commit it honestly
|
|
88
|
+
identifies.
|
|
89
|
+
- **Nothing stops you reusing a tag.** The tag *is* the version's identity, so
|
|
90
|
+
deploying twice as `v1.0.0` leaves two entries in the deploy log that
|
|
91
|
+
odysseus cannot tell apart — and rollback and image retention cannot either.
|
|
92
|
+
This fails silently: you get a history that looks fine and does not say which
|
|
93
|
+
image is on the host.
|
|
94
|
+
|
|
95
|
+
Use `--image` to get started or to test. Use a git repository for anything you
|
|
96
|
+
might later need to roll back or account for.
|
|
97
|
+
|
|
65
98
|
The `--build` flag automatically chooses how to distribute the image:
|
|
66
99
|
- **Without `registry` config** → uses [pussh](https://github.com/psviderski/unregistry) to transfer images directly via SSH
|
|
67
|
-
- **With `registry` config** → pushes to registry, hosts pull from there
|
|
100
|
+
- **With `registry` config** → pushes to registry, hosts pull from there — **experimental**, see [registry](#registry)
|
|
68
101
|
|
|
69
102
|
## Commands
|
|
70
103
|
|
|
104
|
+
### Global options
|
|
105
|
+
|
|
106
|
+
These work with every command:
|
|
107
|
+
|
|
108
|
+
- `--config FILE` - Path to deploy.yml (default: `deploy.yml` in the working directory)
|
|
109
|
+
- `--debug` - Show every command sent to the host, and the connection as it opens.
|
|
110
|
+
`ODYSSEUS_DEBUG=1` does the same. Reach for this first when a command fails
|
|
111
|
+
in a way the message doesn't explain — it prints the literal shell line that
|
|
112
|
+
ran, which is usually where the answer is.
|
|
113
|
+
- `--version` - odysseus, odysseus-core and ruby versions
|
|
114
|
+
|
|
115
|
+
`-v` / `--verbose` is accepted by `deploy`, `build`, `pussh` and `setup`, and
|
|
116
|
+
means the same as `--debug` for those commands.
|
|
117
|
+
|
|
71
118
|
### deploy
|
|
72
119
|
|
|
73
120
|
Deploy all roles to their configured hosts.
|
|
@@ -78,14 +125,14 @@ odysseus deploy [options]
|
|
|
78
125
|
|
|
79
126
|
Options:
|
|
80
127
|
- `--config FILE` - Path to deploy.yml (default: deploy.yml)
|
|
81
|
-
- `--image TAG` - Docker image tag (default:
|
|
128
|
+
- `--image TAG` - Docker image tag (default: the git commit being deployed; required outside a clean git repository). See [Naming the version](#naming-the-version) — it is the quick way, not the equivalent way.
|
|
82
129
|
- `--build` - Build and distribute image before deploying
|
|
83
130
|
- `--dry-run` - Show what would be deployed without doing it
|
|
84
131
|
- `-v, --verbose` - Show SSH commands being executed
|
|
85
132
|
|
|
86
133
|
The `--build` flag automatically chooses the distribution method based on your config:
|
|
87
134
|
- **No `registry` config** → uses pussh (direct SSH transfer to each host)
|
|
88
|
-
- **Has `registry` config** → pushes to registry (hosts pull from there)
|
|
135
|
+
- **Has `registry` config** → pushes to registry (hosts pull from there) — **experimental**, see [registry](#registry)
|
|
89
136
|
|
|
90
137
|
Examples:
|
|
91
138
|
|
|
@@ -107,8 +154,8 @@ odysseus build [options]
|
|
|
107
154
|
|
|
108
155
|
Options:
|
|
109
156
|
- `--config FILE` - Path to deploy.yml (default: deploy.yml)
|
|
110
|
-
- `--image TAG` - Docker image tag (default:
|
|
111
|
-
- `--push` - Push image to registry after build
|
|
157
|
+
- `--image TAG` - Docker image tag (default: the git commit being deployed; required outside a clean git repository). See [Naming the version](#naming-the-version) — it is the quick way, not the equivalent way.
|
|
158
|
+
- `--push` - Push image to registry after build (**experimental**, see [registry](#registry))
|
|
112
159
|
- `--context PATH` - Build context path (default: . relative to deploy.yml)
|
|
113
160
|
- `-v, --verbose` - Show build commands being executed
|
|
114
161
|
|
|
@@ -137,7 +184,7 @@ odysseus pussh [options]
|
|
|
137
184
|
|
|
138
185
|
Options:
|
|
139
186
|
- `--config FILE` - Path to deploy.yml (default: deploy.yml)
|
|
140
|
-
- `--image TAG` - Docker image tag (default:
|
|
187
|
+
- `--image TAG` - Docker image tag (default: the git commit being deployed; required outside a clean git repository). See [Naming the version](#naming-the-version) — it is the quick way, not the equivalent way.
|
|
141
188
|
- `--build` - Build image before pushing
|
|
142
189
|
- `-v, --verbose` - Show commands being executed
|
|
143
190
|
|
|
@@ -188,14 +235,36 @@ Options:
|
|
|
188
235
|
- `-n, --lines N` - Number of lines to show (default: 100)
|
|
189
236
|
- `--since TIME` - Show logs since timestamp (e.g., '10m', '2h')
|
|
190
237
|
|
|
238
|
+
Stopped containers are included, since the container that has just exited is
|
|
239
|
+
usually the one whose logs you want; when the only match is stopped, the
|
|
240
|
+
command says so before printing them. That notice, and the message when no
|
|
241
|
+
container is found at all, go to stderr, so `odysseus logs web1 > app.log`
|
|
242
|
+
captures the logs and nothing else. Finding no container at all — running or
|
|
243
|
+
stopped — exits non-zero, and the message names the role, the label it
|
|
244
|
+
searched for and the roles this config has.
|
|
245
|
+
|
|
191
246
|
### cleanup
|
|
192
247
|
|
|
193
|
-
|
|
248
|
+
Tears the service down on one server. The name undersells it: this is not a
|
|
249
|
+
sweep of stale containers, it is a removal.
|
|
194
250
|
|
|
195
251
|
```bash
|
|
196
252
|
odysseus cleanup <server> [--prune-images]
|
|
197
253
|
```
|
|
198
254
|
|
|
255
|
+
It stops and force-removes **every** container belonging to the service on that
|
|
256
|
+
server — every role, and every dependency, including databases — not only the
|
|
257
|
+
old ones and not excluding the container currently serving traffic. It then
|
|
258
|
+
removes the service's Caddy routes, and if no other service is left behind the
|
|
259
|
+
proxy it stops and removes the shared `odysseus-caddy` container as well, which
|
|
260
|
+
serves every other app on that host.
|
|
261
|
+
|
|
262
|
+
`--prune-images` additionally prunes dangling images.
|
|
263
|
+
|
|
264
|
+
There is no confirmation prompt and nothing is backed up first. Volumes survive,
|
|
265
|
+
so a dependency's data is still there for a later `dependency boot`, but the
|
|
266
|
+
containers are gone.
|
|
267
|
+
|
|
199
268
|
### validate
|
|
200
269
|
|
|
201
270
|
Validate your deploy.yml configuration.
|
|
@@ -204,26 +273,205 @@ Validate your deploy.yml configuration.
|
|
|
204
273
|
odysseus validate [--config FILE]
|
|
205
274
|
```
|
|
206
275
|
|
|
207
|
-
|
|
276
|
+
This loads `plugins:`/`sails:` before checking anything else, the same as
|
|
277
|
+
every other command, so it also catches a plugin gem that is not installed
|
|
278
|
+
on this machine — see `plugins` in the configuration reference below.
|
|
279
|
+
|
|
280
|
+
### setup
|
|
208
281
|
|
|
209
|
-
|
|
282
|
+
Prepares every host in the config so odysseus can deploy to it as a
|
|
283
|
+
non-root user: creates the user `ssh.user` names, adds it to the `docker`
|
|
284
|
+
group, installs your public key, and creates its state directory under
|
|
285
|
+
that user's home (`~/.odysseus`). It only **adds** access — it never
|
|
286
|
+
modifies root's configuration or the bootstrap user's.
|
|
210
287
|
|
|
211
288
|
```bash
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
289
|
+
odysseus setup [--config FILE] [--as USER] [--key PATH]
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
Options:
|
|
293
|
+
- `--as USER` - Identity to connect as while preparing the host (default:
|
|
294
|
+
`ubuntu`, the user Ubuntu's LTS cloud images ship, with passwordless sudo
|
|
295
|
+
already configured). `--as root` connects as root and needs no sudo at
|
|
296
|
+
all. Passwordless sudo is a hard requirement for any other identity —
|
|
297
|
+
odysseus cannot answer a password prompt, so a host without it is
|
|
298
|
+
refused before anything is changed.
|
|
299
|
+
- `--key PATH` - Install this public key instead of the one resolved from
|
|
300
|
+
`ssh.keys`. Repeatable.
|
|
301
|
+
|
|
302
|
+
It reads no new configuration keys: the user comes from `ssh.user`, the
|
|
303
|
+
hosts from `servers.*.hosts`, and the keys from `ssh.keys` — each entry's
|
|
304
|
+
`.pub` sibling if one has a valid key line in it, otherwise derived from
|
|
305
|
+
the private key itself with `ssh-keygen -y`. A `--key` path that doesn't
|
|
306
|
+
resolve to a valid public key refuses, naming the path, rather than
|
|
307
|
+
falling back to `ssh.keys`; the same goes for a `.pub` sibling that has
|
|
308
|
+
content but no line in it validates — for example a restricted
|
|
309
|
+
`command="..." ssh-ed25519 ...` entry, which setup deliberately does not
|
|
310
|
+
install. Setup installs plain login keys only, and it will not silently
|
|
311
|
+
substitute a different key for the one you named or the one on disk. Only
|
|
312
|
+
an empty or whitespace-only `.pub` sibling falls through to deriving the
|
|
313
|
+
key from its private half.
|
|
314
|
+
|
|
315
|
+
**Docker is installed if it isn't there.** If `docker info` doesn't answer,
|
|
316
|
+
setup installs it from Docker's own official apt repository, following
|
|
317
|
+
Docker's published instructions for Ubuntu — it does not distinguish "not
|
|
318
|
+
installed" from "installed but stopped" going in, since neither leaves a
|
|
319
|
+
usable daemon. The keyring and the apt sources file it writes are replaced
|
|
320
|
+
whole on every run, never appended, so a run interrupted partway through
|
|
321
|
+
leaves a stale file the next run overwrites rather than a corrupt one with
|
|
322
|
+
the repository listed twice. apt itself runs non-interactively with a
|
|
323
|
+
300-second wait for the dpkg lock — long enough to outlast cloud-init or
|
|
324
|
+
unattended-upgrades on a host that's only minutes old — and a timeout
|
|
325
|
+
names the process holding it rather than failing silently. The GPG key's
|
|
326
|
+
fingerprint is deliberately not pinned: Docker's own instructions trust
|
|
327
|
+
TLS rather than pin it, and pinning here would turn Docker's routine key
|
|
328
|
+
rotation into an outage for everyone running this command. None of this
|
|
329
|
+
repairs an apt or dpkg state setup didn't create — a host with a broken
|
|
330
|
+
apt is reported, not fixed. Either way, success is decided by `docker
|
|
331
|
+
info` answering after the install runs, not by apt exiting zero. Ubuntu
|
|
332
|
+
24.04 and 26.04 are the only distros it knows; anything else is refused by
|
|
333
|
+
name too — unlike `doctor`, which only warns on an unsupported distro,
|
|
334
|
+
because a deploy just needs a working Docker daemon and doesn't care which
|
|
335
|
+
distro provides it. Setup is stricter because it changes the host: it
|
|
336
|
+
stops at the first thing it can't verify rather than proceeding on a guess.
|
|
337
|
+
|
|
338
|
+
It reports what it changed separately from what was already correct, and
|
|
339
|
+
running it twice against an already-prepared host changes nothing on
|
|
340
|
+
either run.
|
|
341
|
+
|
|
342
|
+
The last thing it does is open a second connection — as the user it just
|
|
343
|
+
created, not the bootstrap identity — and prove Docker and the state
|
|
344
|
+
directory both work from there. It reports success only if that passes. A
|
|
345
|
+
failure at this step leaves the bootstrap path, and everything already
|
|
346
|
+
prepared, untouched: the host stays reachable and there is always a way
|
|
347
|
+
back in to try again.
|
|
348
|
+
|
|
349
|
+
**What it's for.** `setup` gets a single host ready for odysseus to deploy
|
|
350
|
+
to. Preparing servers at scale — many hosts, built from scratch — belongs
|
|
351
|
+
to OpenTofu, Terraform or an equivalent tool that can do it declaratively;
|
|
352
|
+
`setup` is not a substitute for that, only for getting one host going.
|
|
353
|
+
However a host was prepared, including one a provisioning tool built, run
|
|
354
|
+
[`doctor`](#doctor) afterwards to check it.
|
|
355
|
+
|
|
356
|
+
### doctor
|
|
357
|
+
|
|
358
|
+
Read-only diagnosis of every host in the config, connecting as the user
|
|
359
|
+
`ssh.user` names — not root. That's the point: a host that is perfectly fine
|
|
360
|
+
for root can be unusable for a deploy user, and this is the identity odysseus
|
|
361
|
+
will actually deploy as.
|
|
362
|
+
|
|
363
|
+
```bash
|
|
364
|
+
odysseus doctor [--config FILE]
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
For each host it reports:
|
|
368
|
+
|
|
369
|
+
- **distro** — informational only. An unsupported distro is a **warning**,
|
|
370
|
+
not a failure: odysseus deploys to any host with a working Docker daemon,
|
|
371
|
+
and nothing here depends on which distro it is.
|
|
372
|
+
- **docker** — whether the Docker daemon answers as the deploy user.
|
|
373
|
+
- **docker group membership** — whether the deploy user can reach the docker
|
|
374
|
+
socket (skipped for root, which needs no group membership).
|
|
375
|
+
- **state directory** — whether odysseus's state directory, or its nearest
|
|
376
|
+
existing ancestor, is writable by the deploy user.
|
|
377
|
+
- **deploy-log location** — where `odysseus rollback` will read and write
|
|
378
|
+
this service's deploy history, and whether older history exists at a
|
|
379
|
+
location the deploy user can read but no longer write to.
|
|
380
|
+
|
|
381
|
+
Only a failing check sets a non-zero exit; a warning does not. If a check
|
|
382
|
+
itself blows up — a dropped connection mid-host, say — that host is reported
|
|
383
|
+
and the survey moves on to the rest rather than aborting.
|
|
384
|
+
|
|
385
|
+
`doctor` changes nothing on the host: no directory is created, no package is
|
|
386
|
+
installed, nothing is repaired. Caddy's directory is deliberately not
|
|
387
|
+
checked — it doesn't exist until the first deploy starts Caddy, so checking
|
|
388
|
+
for it would report a correctly configured, not-yet-deployed host as broken.
|
|
389
|
+
|
|
390
|
+
**What it's for.** Preparing servers at scale — users, Docker, firewall
|
|
391
|
+
ports, everything declaratively and repeatably — belongs to OpenTofu,
|
|
392
|
+
Terraform or an equivalent tool, not to odysseus. [`setup`](#setup) does a
|
|
393
|
+
deliberately narrow slice of that: a trial-scale bootstrap to get going, not
|
|
394
|
+
the declarative provisioning at scale a tool like that is for; it opens no
|
|
395
|
+
ports, and it is not a provisioning tool. `doctor` answers whether a host is
|
|
396
|
+
actually usable by odysseus as the user your config names, which is worth
|
|
397
|
+
asking however the host was prepared, and can serve as the acceptance test
|
|
398
|
+
for a tofu-built one.
|
|
399
|
+
|
|
400
|
+
### rollback
|
|
401
|
+
|
|
402
|
+
Return every role on every host to a previously deployed version.
|
|
403
|
+
|
|
404
|
+
```bash
|
|
405
|
+
odysseus rollback # to the previous version
|
|
406
|
+
odysseus rollback abc123def456 # to a specific version
|
|
407
|
+
odysseus rollback --list # what each host could roll back to
|
|
408
|
+
```
|
|
409
|
+
|
|
410
|
+
The target is chosen from what the hosts have, not from your checkout: the
|
|
411
|
+
version must still have an image on **every** host, or the rollback refuses
|
|
412
|
+
without touching any of them.
|
|
413
|
+
|
|
414
|
+
A rollback re-runs the deploy path, so it starts a container and waits for
|
|
415
|
+
health checks — roughly the time of a normal deploy, minus build and transfer.
|
|
416
|
+
|
|
417
|
+
Only versions deployed by odysseus 0.4.2 or later can be rolled back to.
|
|
418
|
+
Earlier deploys were built from `:latest`, so no image identifies them.
|
|
419
|
+
|
|
420
|
+
### dependency
|
|
421
|
+
|
|
422
|
+
Manage dependencies (databases, Redis, etc). `dep` is accepted as shorthand.
|
|
423
|
+
|
|
424
|
+
**Renamed from `accessory`.** The old command name and the old `accessories:`
|
|
425
|
+
key in deploy.yml both still work, and the command prints a notice when you use
|
|
426
|
+
the old name. They will be removed in a later release, so rename the key in your
|
|
427
|
+
deploy.yml when convenient:
|
|
428
|
+
|
|
429
|
+
```yaml
|
|
430
|
+
dependencies: # was: accessories:
|
|
431
|
+
db:
|
|
432
|
+
image: postgres:16
|
|
433
|
+
```
|
|
434
|
+
|
|
435
|
+
Nothing on your hosts changes when you rename the key. Container names and the
|
|
436
|
+
`odysseus.service` label are built from the service name plus the individual
|
|
437
|
+
dependency's name, so `db` stays `myapp-db` either way and running containers
|
|
438
|
+
are adopted rather than orphaned.
|
|
439
|
+
|
|
440
|
+
```bash
|
|
441
|
+
# These commands use hosts from dependency config (no server argument needed)
|
|
442
|
+
odysseus dependency boot --name db
|
|
443
|
+
odysseus dependency boot-all
|
|
444
|
+
odysseus dependency remove --name db
|
|
445
|
+
odysseus dependency restart --name db
|
|
446
|
+
odysseus dependency upgrade --name db
|
|
447
|
+
odysseus dependency status
|
|
219
448
|
|
|
220
449
|
# These commands require a server argument
|
|
221
|
-
odysseus
|
|
222
|
-
odysseus
|
|
223
|
-
odysseus
|
|
450
|
+
odysseus dependency logs <server> --name db [-f] [-n 100]
|
|
451
|
+
odysseus dependency exec <server> --name db --command "psql -U postgres"
|
|
452
|
+
odysseus dependency shell <server> --name db
|
|
224
453
|
```
|
|
225
454
|
|
|
226
|
-
|
|
455
|
+
Dependency commands like `boot`, `remove`, `restart`, `upgrade`, and `status` read the target hosts from the dependency's `hosts` configuration in deploy.yml, similar to how `deploy` works. Only `logs`, `exec`, and `shell` require a server argument since they operate on a specific host.
|
|
456
|
+
|
|
457
|
+
**Removing one: `remove` first, then edit deploy.yml.** Every dependency command
|
|
458
|
+
finds its containers through that dependency's config block, so deleting the
|
|
459
|
+
block first strands the container — `remove` then answers `Dependency 'redis'
|
|
460
|
+
not found in config`, and nothing else will find it either. `boot-all` only
|
|
461
|
+
boots what the config lists; it never removes what the config has stopped
|
|
462
|
+
listing, deliberately, because a typo or a half-merged branch would otherwise
|
|
463
|
+
destroy a database. If you have already deleted the block, put it back, run
|
|
464
|
+
`remove`, then delete it.
|
|
465
|
+
|
|
466
|
+
**Volumes outlive `remove`.** It stops and removes containers and nothing else,
|
|
467
|
+
so a named volume — and the data in it — survives, which is what you want when
|
|
468
|
+
replacing a container and not what you want when you meant to be rid of it.
|
|
469
|
+
Removing the data is a separate, deliberate step on the host:
|
|
470
|
+
`docker volume rm myapp-db-data`.
|
|
471
|
+
|
|
472
|
+
A container whose config block is gone keeps running, and keeps restarting,
|
|
473
|
+
without appearing in `dependency status` — which lists what the config
|
|
474
|
+
declares, not what the host is running.
|
|
227
475
|
|
|
228
476
|
### app
|
|
229
477
|
|
|
@@ -235,6 +483,47 @@ odysseus app exec <server> --command "rails db:migrate"
|
|
|
235
483
|
odysseus app console <server> [--cmd "rails c"]
|
|
236
484
|
```
|
|
237
485
|
|
|
486
|
+
Options:
|
|
487
|
+
- `--role ROLE` - Role whose running image to use: web, jobs, etc (default: web)
|
|
488
|
+
|
|
489
|
+
Each of these runs a new container from the image the named role is currently
|
|
490
|
+
running on that host. Containers are labelled per role, so `--role` is required
|
|
491
|
+
to reach anything but web — including on a service that has no web role at all,
|
|
492
|
+
where the default matches nothing on any host:
|
|
493
|
+
|
|
494
|
+
```bash
|
|
495
|
+
odysseus app exec worker1.example.com --role jobs --command "rails runner …"
|
|
496
|
+
```
|
|
497
|
+
|
|
498
|
+
The container is given the same environment a deploy gives it — `env.clear` and
|
|
499
|
+
`env.secret` both — so `odysseus app exec web1 --command "rails db:migrate"`
|
|
500
|
+
talks to the same database as the app running beside it. The values travel in an
|
|
501
|
+
env file rather than on the command line; see [env](#env) for what that means
|
|
502
|
+
and for the one case where the file is left behind.
|
|
503
|
+
|
|
504
|
+
`shell` and `console` print a header before handing the terminal over — the
|
|
505
|
+
server, the role, the image that is serving and the command being run — because
|
|
506
|
+
the prompt you land on tells you none of it:
|
|
507
|
+
|
|
508
|
+
```
|
|
509
|
+
App Shell
|
|
510
|
+
Server: dedalus-prod
|
|
511
|
+
Role: web
|
|
512
|
+
Image: dedalus-production:v1.4.2
|
|
513
|
+
Command: /bin/sh
|
|
514
|
+
› New container from that image: the running app is untouched, and this one is discarded on exit.
|
|
515
|
+
```
|
|
516
|
+
|
|
517
|
+
That last line is the point. These commands `docker run` the serving image; they
|
|
518
|
+
do not attach to the container taking traffic. Nothing you do inside reaches the
|
|
519
|
+
running app, and the container is removed when you leave. `dependency shell` is
|
|
520
|
+
the other way round — it `docker exec`s into the running dependency, so what you
|
|
521
|
+
do there is live.
|
|
522
|
+
|
|
523
|
+
The header goes to **stderr**, so a session whose output you are capturing —
|
|
524
|
+
`odysseus app console web1 --cmd "rails runner 'puts Thing.count'" > count` —
|
|
525
|
+
gets the session's own output on stdout and nothing else.
|
|
526
|
+
|
|
238
527
|
### secrets
|
|
239
528
|
|
|
240
529
|
Manage encrypted secrets files.
|
|
@@ -265,6 +554,46 @@ The name of your service. Used for container naming and Caddy routing.
|
|
|
265
554
|
|
|
266
555
|
The Docker image name (without tag). Tags are specified at deploy time.
|
|
267
556
|
|
|
557
|
+
### plugins
|
|
558
|
+
|
|
559
|
+
Some features ship as separate gems ("sails") instead of being built into
|
|
560
|
+
Odysseus — a deploy strategy, a way of resolving a role's hosts. Having the
|
|
561
|
+
gem is not enough by itself: it also has to be named here, because Odysseus
|
|
562
|
+
never auto-discovers what happens to be installed. That is deliberate — the
|
|
563
|
+
same deploy.yml should behave identically on every machine, whether or not
|
|
564
|
+
some other gem is sitting in the local bundle.
|
|
565
|
+
|
|
566
|
+
```yaml
|
|
567
|
+
plugins:
|
|
568
|
+
- odysseus-sail-example
|
|
569
|
+
```
|
|
570
|
+
|
|
571
|
+
`sails:` is accepted as the same key under a different name; a deploy.yml
|
|
572
|
+
carrying both is a config error, not a silent preference of one over the
|
|
573
|
+
other. Each name is `require`d before the rest of deploy.yml is checked —
|
|
574
|
+
that ordering is what lets `servers.<role>.deploy.strategy` below resolve to
|
|
575
|
+
a strategy the plugin registers. A name that will not `require` (not
|
|
576
|
+
installed, or misspelled) stops validation with an error that names the gem,
|
|
577
|
+
rather than failing later at deploy time. `odysseus validate` runs this same
|
|
578
|
+
loading step, so it catches a missing plugin gem too — which means
|
|
579
|
+
`validate` can fail on a machine that lacks the gem where it used to pass,
|
|
580
|
+
if your deploy.yml lists one.
|
|
581
|
+
|
|
582
|
+
**No sail gem is published to RubyGems.** `odysseus-sail-example` above is a
|
|
583
|
+
placeholder, not a gem you can install. The sails that exist live in their
|
|
584
|
+
own repositories alongside this one, so a deploy.yml that names one only
|
|
585
|
+
works where the gem is reachable from your app's Gemfile:
|
|
586
|
+
|
|
587
|
+
```ruby
|
|
588
|
+
# Gemfile — one or the other, not both
|
|
589
|
+
gem 'odysseus-sail-example', git: 'https://example.com/odysseus-sail-example.git'
|
|
590
|
+
gem 'odysseus-sail-example', path: '../odysseus-sail-example'
|
|
591
|
+
```
|
|
592
|
+
|
|
593
|
+
The failure a plugin that will not load raises suggests `gem install <name>`.
|
|
594
|
+
That is the right advice for a published gem, and no help for a sail: the fix
|
|
595
|
+
is the Gemfile entry above.
|
|
596
|
+
|
|
268
597
|
### servers
|
|
269
598
|
|
|
270
599
|
Define roles and their target hosts:
|
|
@@ -293,39 +622,69 @@ Available options:
|
|
|
293
622
|
- `cpus` - CPU limit (e.g., `2` for 2 cores, `1.5` for 1.5 cores)
|
|
294
623
|
- `cpu_shares` - Relative CPU weight (default: 1024)
|
|
295
624
|
|
|
296
|
-
|
|
625
|
+
**SSH configuration** (bastions, ProxyJump, etc.) is your responsibility. Odysseus only needs the hostnames/IPs and relies on your local SSH config.
|
|
626
|
+
|
|
627
|
+
Every role must carry a `hosts` array, or config validation rejects it with
|
|
628
|
+
`server role 'web' must have 'hosts' array`. A sail that resolves a role's
|
|
629
|
+
hosts for you — see `plugins` above — still needs the key present; it may be
|
|
630
|
+
empty, and the sail fills it in at deploy time.
|
|
297
631
|
|
|
298
|
-
|
|
632
|
+
#### containers
|
|
633
|
+
|
|
634
|
+
Run more than one container per host for a role — meaningful only to a
|
|
635
|
+
strategy that reads it. The built-in strategy always runs exactly one
|
|
636
|
+
container per role per host and ignores this block:
|
|
299
637
|
|
|
300
638
|
```yaml
|
|
301
639
|
servers:
|
|
302
640
|
web:
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
options:
|
|
309
|
-
memory: 4g
|
|
641
|
+
deploy:
|
|
642
|
+
strategy: example
|
|
643
|
+
containers:
|
|
644
|
+
count: 3 # containers per host on this role (default: 1)
|
|
645
|
+
name_pattern: "web-%d" # default for every role — see warning below
|
|
310
646
|
```
|
|
311
647
|
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
648
|
+
**`name_pattern` defaults to `"web-%d"` for every role, and a container's
|
|
649
|
+
name is built from the bare service name, not the role**
|
|
650
|
+
(`<service>-<name_pattern % slot>`). Two roles that both leave it at the
|
|
651
|
+
default — say `web` and a multi-container `jobs`, on the same multi-container
|
|
652
|
+
strategy and deployed to the same host — produce identically-named
|
|
653
|
+
containers, and each role's deploy will see and manage the other's
|
|
654
|
+
containers. Give every role beyond the first its own `name_pattern` (e.g.
|
|
655
|
+
`"jobs-%d"`) whenever more than one role runs multi-container on a shared
|
|
656
|
+
host.
|
|
321
657
|
|
|
322
|
-
|
|
323
|
-
```ruby
|
|
324
|
-
gem 'aws-sdk-autoscaling'
|
|
325
|
-
gem 'aws-sdk-ec2'
|
|
326
|
-
```
|
|
658
|
+
#### deploy
|
|
327
659
|
|
|
328
|
-
|
|
660
|
+
```yaml
|
|
661
|
+
servers:
|
|
662
|
+
web:
|
|
663
|
+
deploy:
|
|
664
|
+
strategy: example # optional — see `plugins` above; omit for the built-in strategy
|
|
665
|
+
drain_timeout: 30 # seconds to wait for in-flight connections before stopping the old container
|
|
666
|
+
stop_timeout: 10 # seconds of grace before the old container is force-removed
|
|
667
|
+
boot_timeout: 60 # seconds to wait for a new container to become healthy
|
|
668
|
+
health_check:
|
|
669
|
+
path: /up # default: /up
|
|
670
|
+
interval: 2 # seconds between checks (default: 2)
|
|
671
|
+
threshold: 3 # consecutive successes required (default: 3)
|
|
672
|
+
timeout: 5 # seconds per check (default: 5)
|
|
673
|
+
```
|
|
674
|
+
|
|
675
|
+
`strategy` chooses the orchestrator for the role. Leave it out for
|
|
676
|
+
Odysseus's built-in zero-downtime replacement (start new, health-check via
|
|
677
|
+
`proxy.healthcheck`, switch traffic, drain and stop old — its own timings,
|
|
678
|
+
not the ones below). Name a strategy a plugin registers — currently only
|
|
679
|
+
`rolling` — to use that instead; it must be loaded via `plugins:` first, or
|
|
680
|
+
config validation refuses it with "is not registered — is the sail plugin
|
|
681
|
+
gem loaded?".
|
|
682
|
+
|
|
683
|
+
`drain_timeout`, `stop_timeout`, `boot_timeout` and `health_check` are parsed
|
|
684
|
+
and shape-checked regardless of `strategy`, but **the built-in strategy does
|
|
685
|
+
not read them** — it drains for a fixed 5 seconds and waits up to a fixed 60
|
|
686
|
+
seconds for Docker's own health check, neither of which this block changes.
|
|
687
|
+
They exist for a strategy that chooses to read them; `rolling` does.
|
|
329
688
|
|
|
330
689
|
### proxy
|
|
331
690
|
|
|
@@ -346,6 +705,9 @@ proxy:
|
|
|
346
705
|
expect_status: 200 # Optional: expected HTTP status (default: 2xx)
|
|
347
706
|
```
|
|
348
707
|
|
|
708
|
+
A web container must report healthy before Odysseus routes traffic to it. If
|
|
709
|
+
you omit `healthcheck`, the container is probed with `GET /` on `app_port`.
|
|
710
|
+
|
|
349
711
|
### env
|
|
350
712
|
|
|
351
713
|
Environment variables:
|
|
@@ -362,6 +724,27 @@ env:
|
|
|
362
724
|
- `clear` - Plaintext values stored in deploy.yml
|
|
363
725
|
- `secret` - Keys to load from encrypted secrets file or server environment
|
|
364
726
|
|
|
727
|
+
Both are handed to the container through an env file written under the state
|
|
728
|
+
directory described in [ssh](#ssh) — `/var/lib/odysseus/env` for the default
|
|
729
|
+
root connection — with `0600` permissions, and removed once the container has
|
|
730
|
+
been created, so secrets never appear in the host's process list. A value
|
|
731
|
+
containing a newline is rejected, since a Docker env file cannot represent one.
|
|
732
|
+
|
|
733
|
+
`app exec`, `app shell` and `app console` get the same environment the same way.
|
|
734
|
+
An interactive session holds its env file for as long as the session lasts and
|
|
735
|
+
removes it on the way out, whether the session ended cleanly, exited non-zero or
|
|
736
|
+
was interrupted. A session that sits idle long enough for its ssh connection to
|
|
737
|
+
be dropped — an idle NAT timeout, an `sshd` `ClientAlive` limit, a Tailscale
|
|
738
|
+
relay change — is included: the file is removed over a fresh connection.
|
|
739
|
+
|
|
740
|
+
Two cases still leave the file on the host. The `odysseus` process being killed
|
|
741
|
+
outright — `SIGKILL`, or the machine going down — where no cleanup can run at
|
|
742
|
+
all; and a host that is unreachable when the session ends, where there is
|
|
743
|
+
nowhere to send the removal. The file is mode `0600` inside that same env
|
|
744
|
+
directory, which is `0700` for every connection, so another user on the box
|
|
745
|
+
still cannot read it; but nothing comes back to remove it, since the next run
|
|
746
|
+
writes its own file rather than tidying old ones.
|
|
747
|
+
|
|
365
748
|
### secrets_file
|
|
366
749
|
|
|
367
750
|
Path to an encrypted secrets file (relative to deploy.yml or absolute):
|
|
@@ -380,18 +763,18 @@ RAILS_MASTER_KEY: abc123def456
|
|
|
380
763
|
|
|
381
764
|
During deploy, secrets listed in `env.secret` are loaded from the encrypted file. If a key is not found in the secrets file, it falls back to the server's environment variables.
|
|
382
765
|
|
|
383
|
-
###
|
|
766
|
+
### dependencies
|
|
384
767
|
|
|
385
768
|
Long-running services like databases:
|
|
386
769
|
|
|
387
770
|
```yaml
|
|
388
|
-
|
|
771
|
+
dependencies:
|
|
389
772
|
db:
|
|
390
773
|
image: postgres:16
|
|
391
774
|
hosts:
|
|
392
775
|
- db.example.com
|
|
393
776
|
volumes:
|
|
394
|
-
- /
|
|
777
|
+
- /srv/myapp/postgres:/var/lib/postgresql/data
|
|
395
778
|
env:
|
|
396
779
|
clear:
|
|
397
780
|
POSTGRES_USER: myapp
|
|
@@ -402,7 +785,7 @@ accessories:
|
|
|
402
785
|
timeout: 5
|
|
403
786
|
```
|
|
404
787
|
|
|
405
|
-
Each
|
|
788
|
+
Each dependency must define `hosts` - the servers where it should run.
|
|
406
789
|
|
|
407
790
|
### ssh
|
|
408
791
|
|
|
@@ -415,6 +798,20 @@ ssh:
|
|
|
415
798
|
- ~/.ssh/id_ed25519
|
|
416
799
|
```
|
|
417
800
|
|
|
801
|
+
`user` defaults to `root` and also decides where odysseus keeps state on the
|
|
802
|
+
host. A root connection writes to `/var/lib/odysseus`, exactly as always. Any
|
|
803
|
+
other user writes under its own `$HOME/.odysseus` instead, because it cannot
|
|
804
|
+
create or `chmod` a directory root owns. Caddy's certificate directory
|
|
805
|
+
follows the same rule as everything else: `/var/lib/odysseus/caddy` for a
|
|
806
|
+
root connection, `$HOME/.odysseus/caddy` for any other user.
|
|
807
|
+
|
|
808
|
+
A non-root `user` must already exist on the target host — with membership in
|
|
809
|
+
the `docker` group and a writable home directory — and its key must be one of
|
|
810
|
+
`keys` above. [`odysseus setup`](#setup) can create that user, add it to
|
|
811
|
+
`docker`, install the key for you, and install Docker itself if the host
|
|
812
|
+
doesn't have it. A host prepared by a provisioning tool instead of `setup`
|
|
813
|
+
still needs Docker present.
|
|
814
|
+
|
|
418
815
|
### builder
|
|
419
816
|
|
|
420
817
|
Configuration for building Docker images:
|
|
@@ -443,6 +840,9 @@ Build strategies:
|
|
|
443
840
|
|
|
444
841
|
### registry
|
|
445
842
|
|
|
843
|
+
> **Experimental:** registry-based distribution is not officially supported yet. The
|
|
844
|
+
> pussh path is the supported one; treat this as untested ground and keep a way back.
|
|
845
|
+
|
|
446
846
|
Docker registry configuration. When present, `odysseus deploy --build` will push images to the registry instead of using pussh:
|
|
447
847
|
|
|
448
848
|
```yaml
|
|
@@ -461,53 +861,37 @@ registry:
|
|
|
461
861
|
|
|
462
862
|
For better security, you can store registry credentials in your encrypted secrets file and reference them.
|
|
463
863
|
|
|
464
|
-
|
|
864
|
+
### retain_versions
|
|
465
865
|
|
|
466
|
-
|
|
866
|
+
How many distinct versions of your service's image each host keeps. Default 5.
|
|
467
867
|
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
```bash
|
|
471
|
-
# Via command-line flag
|
|
472
|
-
odysseus deploy --charm --build
|
|
473
|
-
|
|
474
|
-
# Via environment variable
|
|
475
|
-
ODYSSEUS_CHARM=1 odysseus deploy --build
|
|
868
|
+
```yaml
|
|
869
|
+
retain_versions: 5
|
|
476
870
|
```
|
|
477
871
|
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
- **Confirmation dialogs** for destructive operations (cleanup, accessory remove)
|
|
484
|
-
|
|
485
|
-
### Installing gum
|
|
872
|
+
After a successful deploy, images outside that window are removed from each
|
|
873
|
+
host. An image is only removed if the host's own deploy log records it, no
|
|
874
|
+
container on the host still references it, and docker accepts the removal —
|
|
875
|
+
so a version you are still running is never deleted, and a failure to delete
|
|
876
|
+
one image never fails the deploy.
|
|
486
877
|
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
# macOS
|
|
491
|
-
brew install gum
|
|
492
|
-
|
|
493
|
-
# Arch Linux
|
|
494
|
-
pacman -S gum
|
|
495
|
-
|
|
496
|
-
# Ubuntu/Debian (via charm tap)
|
|
497
|
-
sudo mkdir -p /etc/apt/keyrings
|
|
498
|
-
curl -fsSL https://repo.charm.sh/apt/gpg.key | sudo gpg --dearmor -o /etc/apt/keyrings/charm.gpg
|
|
499
|
-
echo "deb [signed-by=/etc/apt/keyrings/charm.gpg] https://repo.charm.sh/apt/ * *" | sudo tee /etc/apt/sources.list.d/charm.list
|
|
500
|
-
sudo apt update && sudo apt install gum
|
|
501
|
-
|
|
502
|
-
# From source
|
|
503
|
-
go install github.com/charmbracelet/gum@latest
|
|
504
|
-
```
|
|
878
|
+
Setting this to `1` is allowed but means the previous version's image becomes
|
|
879
|
+
eligible for removal as soon as you deploy, leaving `odysseus rollback` with
|
|
880
|
+
no candidate. Use at least 2 if you want to be able to roll back.
|
|
505
881
|
|
|
506
|
-
|
|
882
|
+
`latest` is never removed automatically. `odysseus cleanup --prune-images`
|
|
883
|
+
only removes *dangling* images, and a tagged `latest` is never dangling —
|
|
884
|
+
removing it means `docker image rm` by hand on the host.
|
|
507
885
|
|
|
508
886
|
## Server Requirements
|
|
509
887
|
|
|
510
|
-
|
|
888
|
+
A host odysseus **deploys** to needs a working Docker daemon and SSH access —
|
|
889
|
+
deploys never gate on distro. [`odysseus setup`](#setup), the optional
|
|
890
|
+
bootstrap for a fresh host, is what requires a supported Ubuntu release
|
|
891
|
+
(24.04 or 26.04); a host prepared some other way — by hand, or by a
|
|
892
|
+
provisioning tool — just needs Docker present already. Odysseus
|
|
893
|
+
automatically deploys and manages Caddy as a container (`odysseus-caddy`) -
|
|
894
|
+
no manual Caddy installation required.
|
|
511
895
|
|
|
512
896
|
## How It Works
|
|
513
897
|
|