# Compose templates and their setup scripts

One directory per provisionable service. Each holds a `docker-compose.yaml` and a `setup.sh`, and
together they are everything needed to bring that service up.

This is deliberately the shape a sidecar will need when it lives in **its own repository**: metadata
(the catalogue entry), a compose template, a setup script, and a schema. Nothing here may assume it is
being read out of this repo.

---

## The setup.sh contract

**Answers come from the environment. It never prompts when they are already there.**

The app store collects them in a web form and passes them as environment variables. A person running it
by hand on a VPS gets prompted for anything missing, but only when stdin is a TTY — so the same script
serves both, and neither is a second code path.

```sh
OFFICER_SERVICE_DIR=~/officerdev/dockers/transmission \
OFFICER_UID=1000 OFFICER_GID=1000 \
TRANSMISSION_PORT=9091 \
bash setup.sh
```

Every script must:

| Rule | Why |
|---|---|
| **Be idempotent.** Running twice must be safe and must not create a second anything. | Install is resumable; a retry after a half-failure re-runs steps that already succeeded. |
| **Never prompt when `OFFICER_NONINTERACTIVE=1`.** Fail with a clear message instead. | A prompt behind a web form is a hang with no output, which is the worst failure to diagnose. |
| **Write only inside `OFFICER_SERVICE_DIR`.** | The app store owns that directory and nothing else. The user's own estate is never touched. |
| **Emit progress on stdout.** | The installer streams it to a terminal panel in the UI, so the user watches it happen rather than staring at a spinner. |
| **Print `OFFICER_RESULT_<KEY>=value` for anything the platform must store.** | How a generated secret or a resolved port gets back to `service_connections` without the installer parsing free text. |

Exit non-zero on failure, with the reason on stderr. The installer records it in `last_error` and the
row stays `failed` rather than pretending to be installed.

---

## Volumes are always relative bind mounts

`./data`, `./config`, `./database` — never named volumes. Configuration and data sit beside the compose
file so both the platform and a human can see exactly what a service keeps and where. A named volume
hides it behind `docker volume inspect`, which is the opposite of the point.

## Containers run as the owner

`user: "${OFFICER_UID}:${OFFICER_GID}"`, so files a container writes are owned by the person who
installed it and not by root. This is the convention already in use across the owner's own services.

## Ports bind to loopback unless the service genuinely needs to be reachable

`127.0.0.1:9091:9091`, not `9091:9091`. Officer reaches these over loopback; anything published on all
interfaces is a service exposed to the network by an installer the user trusted to be careful. The
exception is a service whose whole function is inbound connections — slskd's P2P listener, for example —
and those say so in a comment.

## No external networks

The owner's own composes attach to an external `nginx` network that exists on his machine. Templates
must not require one: a fresh VPS has no such network and `docker compose up` would fail before it
started. Default network only.

---

## Three provisioning shapes, not one

Worth knowing before writing a template, because the design doc originally assumed only the first:

1. **We set the credentials.** Passed as environment to the container, so the connection is fully known
   the moment it is up. Transmission (`USER`/`PASS`), Vaultwarden (`ADMIN_TOKEN`).
2. **We generate a secret into a config file.** The bind mount lets us write it before first boot, so it
   is still known without asking. slskd's API key lives in its `slskd.yml`.
3. **A human must mint a token in the service's own UI after it boots.** Immich, Jellyfin and Memos all
   work this way — there is no environment variable that pre-seeds an API key.

Shape 3 means an install can be **provisioned and running, but not yet connected**. That is a real state,
not an error: the container is up, the compose file is written, and the platform is waiting for a token.
The installer stops there with the step recorded, and the UI asks for the key with a link to the page
that mints it. Resuming finishes the job.
