app store: put provisioned containers under the officer root, not the user's
The install layout a machine should have, seasoned owner or not:
~/officerdev/
platform/ the app
data/ DATA_PATH
dockers/ services the app store provisioned
capabilities/ the file-based item store
One root, everything under it. OFFICER_ROOT derives from DATA_PATH rather than being a second variable
that has to agree with the first.
Deliberately not `~/dockers`, where a seasoned user already keeps their own estate — 47 services on this
machine. That separation buys two things. Containers the app store created are distinguishable from the
user's own structurally, rather than by a naming convention we would have to enforce and they could
break. And we never reason about someone else's compose files: the store does not scan, adopt or modify
anything outside its own directory.
That also simplifies "I already have one of these" — it is answered by the user giving a URL, never by
us finding a directory and guessing whose it is. An earlier draft had the installer adopting existing
directories, which meant reading, and potentially writing over, services Officer did not create.
This development machine predates the convention and derives an ugly-but-correct path, since the project
sits inside ~/dockers/officer.dev. Still isolated, still one root. New installs get the clean shape.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+26
-11
@@ -60,12 +60,11 @@ for someone who does not. The prompt is the fork.
|
||||
|
||||
**Officer is the installer, never the owner.** Concretely:
|
||||
|
||||
- A real compose file per service, written into a **user-owned directory**, from our template —
|
||||
following the convention the owner already uses for 47 services in `~/dockers/`:
|
||||
one directory per service, `docker-compose.yaml` inside, and **relative bind mounts**
|
||||
(`./data`, `./database`, `./storage`) so configuration and data sit beside the compose file where
|
||||
both we and the user can find them. Named volumes are used by 3 of those 47 and are the exception;
|
||||
templates use bind mounts, always.
|
||||
- A real compose file per service, written into **`<root>/dockers/<id>/`**, from our template — using
|
||||
the convention the owner already applies to 47 services: one directory per service,
|
||||
`docker-compose.yaml` inside, and **relative bind mounts** (`./data`, `./database`, `./storage`) so
|
||||
configuration and data sit beside the compose file where both we and a human can see them. Named
|
||||
volumes are used by 3 of those 47 and are the exception; templates use bind mounts, always.
|
||||
- Started with `docker compose up -d` **as the owner**, not as officer's own identity.
|
||||
- Found again by **label** (`officer.sidecar=<id>`), not by holding a handle.
|
||||
|
||||
@@ -82,11 +81,27 @@ health checks already correct, so "install Gitea" does not become a tutorial.
|
||||
**`USER_UID` / `USER_GID` are set to the owner**, as the existing services already do. That answers the
|
||||
"do containers run as root" question: no, and this is not a new convention — it is the one in use.
|
||||
|
||||
**An existing directory is evidence, not an obstacle.** `~/dockers/` already holds `gitea`, `memos`,
|
||||
`immich`, `jellyfin` and `invoice_shelf`. The installer must never write into a directory that exists;
|
||||
finding one is the strongest possible signal that this is the "you already have one" case, and the store
|
||||
should offer to ADOPT it — read its ports out of the compose file and write the connection — rather than
|
||||
provision a second copy or overwrite a running service's data.
|
||||
**The app store's containers are isolated from the user's own**, and that is the point of the layout:
|
||||
|
||||
```
|
||||
~/officerdev/
|
||||
platform/ the app
|
||||
data/ DATA_PATH
|
||||
dockers/ services the app store provisioned <- exclusively ours
|
||||
capabilities/ the file-based item store
|
||||
```
|
||||
|
||||
`OFFICER_ROOT` is derived as the parent of `DATA_PATH` rather than configured separately — a second
|
||||
variable that must agree with the first is a second thing to get wrong.
|
||||
|
||||
Deliberately **not** `~/dockers`, which is where a seasoned user already keeps their estate. Two
|
||||
consequences, both wanted:
|
||||
|
||||
1. Containers the app store created are distinguishable from the user's own **structurally**, not by a
|
||||
naming convention we would have to enforce and they could break.
|
||||
2. **We never reason about someone else's compose files.** The store does not scan, adopt or modify
|
||||
anything outside its own directory. "I already have one of these" is answered by the user giving a
|
||||
URL (`mode: 'existing'`) — never by us finding a directory and guessing whose it is.
|
||||
|
||||
`[open]` Podman, for anyone wanting genuinely rootless.
|
||||
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
import { dirname, join } from 'node:path';
|
||||
import { DATA_PATH } from '../data-path';
|
||||
|
||||
// Where the app store puts the containers it provisions.
|
||||
//
|
||||
// ── The install layout ──
|
||||
//
|
||||
// A machine that runs Officer is meant to look like this, whether the owner is seasoned or not:
|
||||
//
|
||||
// ~/officerdev/
|
||||
// platform/ the app
|
||||
// data/ DATA_PATH — managed homes, attachments, job logs
|
||||
// dockers/ services the app store provisioned <- this file
|
||||
// capabilities/ the file-based item store
|
||||
//
|
||||
// One root, everything under it, nothing scattered. `OFFICER_ROOT` is derived from `DATA_PATH` rather
|
||||
// than configured separately, because a second environment variable that must agree with the first is a
|
||||
// second thing to get wrong — and on a correct install `data/` is always a direct child of the root.
|
||||
//
|
||||
// (This development machine predates the convention and has it inverted: the whole project sits inside
|
||||
// `~/dockers/officer.dev/`, so the root derives to `officer.dev` and the app store's directory would be
|
||||
// `~/dockers/officer.dev/dockers`. Which is ugly, and correct — it is still isolated, still under one
|
||||
// root, and still not mixed in with anything else. New installs get the clean shape.)
|
||||
//
|
||||
// ── Why this is not `~/dockers` ──
|
||||
//
|
||||
// That is where a seasoned user already keeps their own estate — 47 services on this machine alone. Two
|
||||
// reasons to stay out of it:
|
||||
//
|
||||
// 1. **Isolation.** Containers the app store created and containers the user manages must be
|
||||
// distinguishable without inspecting them. A separate root makes that structural rather than a
|
||||
// naming convention we would have to enforce and they could break.
|
||||
// 2. **We never reason about someone else's compose files.** The app store does not scan, adopt or
|
||||
// modify anything outside its own directory. "I already have one of these" is answered by the user
|
||||
// giving a URL (`mode: 'existing'`), never by us finding a directory and guessing it is theirs.
|
||||
//
|
||||
// So this directory is exclusively ours to write, and everything in it was put there by an install.
|
||||
|
||||
/**
|
||||
* The install root — the parent of `data/`. On a conventional install, `~/officerdev`.
|
||||
*
|
||||
* Derived, not configured: see above.
|
||||
*/
|
||||
export const OFFICER_ROOT = dirname(DATA_PATH);
|
||||
|
||||
/** Where provisioned services live, one directory each. Created on first install, not at boot. */
|
||||
export const DOCKERS_DIR = join(OFFICER_ROOT, 'dockers');
|
||||
|
||||
/**
|
||||
* This service's own directory: `<root>/dockers/<id>/`, holding `docker-compose.yaml` and — because the
|
||||
* templates use relative bind mounts rather than named volumes — its data and configuration too.
|
||||
*
|
||||
* That is the convention the owner already uses everywhere: `./data`, `./database`, `./storage` beside
|
||||
* the compose file, so both the app and a human can see exactly what a service is keeping and where.
|
||||
* A named volume hides it behind `docker volume inspect`, which is the opposite of the point.
|
||||
*/
|
||||
export const serviceDir = (sidecarId: string): string => join(DOCKERS_DIR, sidecarId);
|
||||
|
||||
/** The compose file the installer renders and `docker compose` is run against. */
|
||||
export const composeFile = (sidecarId: string): string => join(serviceDir(sidecarId), 'docker-compose.yaml');
|
||||
Reference in New Issue
Block a user