diff --git a/CLAUDE.md b/CLAUDE.md index 6c774d8..5b84c90 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -41,6 +41,27 @@ links out to each service. Per-service detail (variables, ports, storage) belongs in that service's README, not the root one. Adding a service means adding its README and a row to the root table. +## Service READMEs stay inside their directory + +A service's `README.md` describes that service and nothing else. It does not +name, link to, or compare itself with another service, and it does not link up +to the root README or CLAUDE.md. Shared conventions — the workspace volume +split, no published ports, the restart policy, secrets, variable order — are +written once at the root and are not restated or "see the root for why"-linked +from a service. A service README says what the service is, its variables, +storage and wiring, and the reasons behind choices specific to that service. +Keep it concise and minimal. + +This is a deployment rule, not a style preference. Each service is a separate +Coolify app whose webhook watch path is `/**`. A cross-link means +renaming or editing one service touches another's directory and redeploys it. +Cross-cutting changes to every compose file (a new restart policy, say) are the +one legitimate case where a push redeploys several services. + +Every Coolify app created from this repo sets its watch path to `/**`. +An app with no watch path deploys on every push to the repository — +`traffmonetizer` leaves it unset on purpose, to get restarted that often. + ## Workspace services A service someone works *inside* — an editor, a coding agent, anything with a diff --git a/README.md b/README.md index 6579525..eebe850 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,11 @@ README. Compose names the project after its directory, so `code-server/` comes up as the `code-server` project with its own network and volumes. +Each service README covers only its own service. Shared conventions live here +and are not repeated or linked from a service, so editing one service never +touches another's directory — each is a separate Coolify app deploying on a +`/**` watch path. + ## Usage In Coolify or Dokploy, point a Docker Compose resource at the service directory diff --git a/alloy/README.md b/alloy/README.md index 1f46c35..c4b9fa6 100644 --- a/alloy/README.md +++ b/alloy/README.md @@ -7,11 +7,13 @@ Management. One container runs both the `node_exporter` (host) and `cadvisor` (container) collectors. A second, tiny container proxies a read-only slice of the Docker API to it. The Alloy config is embedded inline via Compose `configs:`, so -there is no `config.alloy` on disk, and every setting comes from a shell -variable rather than a `.env` file. +there is no `config.alloy` on disk. There is no `.env.example` either: the +nine variables are exported before `docker compose up`. -Uses `docker-compose.yml`, and sets `container_name: alloy` — unlike the -platform-managed services described in the [root README](../README.md). +The compose file is `docker-compose.yml`. Both containers have fixed names, +`container_name: alloy` and `alloy-dockerproxy`, and `dockerproxy` publishes +`127.0.0.1:2375` — Alloy runs with host networking, so it has no compose +network to reach the proxy over. ## What it collects @@ -103,7 +105,7 @@ secret-bearing — it holds `GRAFANA_TOKEN` regardless. `prometheus.exporter.cadvisor` and `loki.source.docker` both need the Docker API, so it cannot simply be removed. `dockerproxy` runs [tecnativa/docker-socket-proxy](https://github.com/Tecnativa/docker-socket-proxy) -with the socket mounted read-only and exposes it on `127.0.0.1:2375`, which +with the socket mounted read-only and publishes it on `127.0.0.1:2375`, which Alloy reaches over host networking. `POST` is revoked by default in that image, which is the point: container diff --git a/alloy/docker-compose.yml b/alloy/docker-compose.yml index 7f1f643..d478d42 100644 --- a/alloy/docker-compose.yml +++ b/alloy/docker-compose.yml @@ -171,7 +171,7 @@ configs: } // File-based logs from the Linux-Node integration: syslog/messages/*.log. - // On systemd hosts, rsyslog often mirrors journald — see README "Caveats". + // On systemd hosts, rsyslog often mirrors journald — see README "What it collects". local.file_match "integrations_node_exporter_files" { path_targets = [{ __address__ = "localhost", diff --git a/code-server/README.md b/code-server/README.md index ddc398c..13585e7 100644 --- a/code-server/README.md +++ b/code-server/README.md @@ -3,10 +3,10 @@ [VS Code in the browser](https://github.com/linuxserver/docker-code-server), from the LinuxServer image, set up as a full remote dev box. -Comes with Go, Node.js 24, Python 3, and zsh via LinuxServer mods, plus -`bubblewrap`, `gh`, `git`, `glab`, `unzip` and `zip` through -`INSTALL_PACKAGES`. Git -author/committer identity is injected from `.env`. +Comes with Go, Node.js 24, Python 3 and zsh via LinuxServer mods, the +`code-server-npmglobal` mod so `npm install -g` lands under `/config` and persists, +plus `bubblewrap`, `gh`, `git`, `glab`, `unzip` and `zip` through +`INSTALL_PACKAGES`. Git author/committer identity is injected from `.env`. ## Docker access @@ -19,7 +19,7 @@ not exist unless the same path exists on the host. The socket is owned by the host's `docker` group, which the `abc` user inside the container is not a member of; run `docker` under `sudo` (the `SUDO_PASSWORD` is the same `PASSWORD`) or add the group by hand. Handing a container the -socket is equivalent to giving it root on the host -- that is accepted here +socket is equivalent to giving it root on the host — that is accepted here because this is a single-user dev box. The mount carries `:ro`, which is not a security boundary: it only marks the @@ -41,20 +41,17 @@ Generate a password with `openssl rand -base64 24`. `SERVICE_HOSTNAME` is used twice: as the container's `hostname:` and as the `HOST` variable inside it. Coolify injects `HOST=0.0.0.0` into every compose app, and zsh seeds `$HOST` and the `%m`/`%M` prompt escapes from that variable -rather than calling `gethostname()` -- so the prompt reads `0`, the first -dot-separated field of `0.0.0.0`. code-server itself never reads `HOST` -- it -binds `[::]:8443` -- so overriding it only affects the prompt. bash is +rather than calling `gethostname()` — so the prompt reads `0`, the first +dot-separated field of `0.0.0.0`. code-server itself never reads `HOST` — it +binds `[::]:8443` — so overriding it only affects the prompt. bash is unaffected; its `\h` uses the real hostname. -It is not called `HOSTNAME`, the obvious name, because Compose interpolation -lets the deploying shell's environment win over the `.env` file, and `HOSTNAME` -is set in every container -- including the one Coolify itself runs in. The -container would silently take Coolify's hostname instead of this value. +`PUID`/`PGID` are pinned to `1000` in `compose.yml`; the `Dockerfile` depends +on that (see [Storage](#storage)). ## Networking -Listens on `8443`. No ports are published — point the domain at that port in -Coolify or Dokploy. See the [root README](../README.md) for why. +Listens on `8443`; point the domain at it. ## Storage @@ -63,31 +60,23 @@ Coolify or Dokploy. See the [root README](../README.md) for why. | `code-server-config` | `/config` | Home directory: settings, extensions, shell history, CLI logins | | `code-server-workspace` | `/workspace` | Code you work on | -Two volumes, the same split [paseo](../paseo/README.md) and -[opencode](../opencode/README.md) use: home in one, the workspace in -the other. Code survives a wipe of the editor's state, and the editor's state -survives a wipe of the code. - -`DEFAULT_WORKSPACE` points at `/workspace` to match. It only chooses the folder +`DEFAULT_WORKSPACE` points at `/workspace`. It only chooses the folder code-server opens; it does not move anything. -The `Dockerfile` exists only because of that move. The image hard-codes what it -hands to the `abc` user — `init-adduser` takes `/app`, `/config` and -`/defaults`, `init-code-server` takes `/config/workspace` by literal path — and -reads `DEFAULT_WORKSPACE` only to decide which folder to open. A named volume -on `/workspace` is therefore never chowned, comes up `root:root`, and the -editor cannot write a single file into it. +The `Dockerfile` exists only to make `/workspace` writable. The image +hard-codes what it hands to the `abc` user — `init-adduser` takes `/app`, +`/config` and `/defaults`, `init-code-server` takes `/config/workspace` by +literal path — and reads `DEFAULT_WORKSPACE` only to decide which folder to +open. A named volume on `/workspace` is therefore never chowned, comes up +`root:root`, and the editor cannot write a single file into it. -Creating the directory in the image fixes it without any runtime step: Docker -seeds an empty named volume from the image directory, ownership included, so -`/workspace` arrives owned by `abc`. It is the same reason `paseo` needs no -fixup — its upstream image ships `/workspace` already owned. +Creating the directory in the image, owned by `1000:1000`, fixes it without any +runtime step: Docker seeds an empty named volume from the image directory, +ownership included, so `/workspace` arrives owned by `abc`. The alternative was a `chown` script in `/custom-cont-init.d`, the image's own init hook. It was rejected because it needs a bind mount from the repository into the container, and because the hook silently skips any script that has lost its executable bit — a read-only workspace with nothing obvious to blame. Baking `1000:1000` into the image costs the ability to change `PUID` at -runtime, which is free here: both services pin it to `1000`. - -Anything outside these two volumes is lost on redeploy. +runtime, which is free here because `compose.yml` pins it. diff --git a/couchbase/README.md b/couchbase/README.md index 839a6e6..2044455 100644 --- a/couchbase/README.md +++ b/couchbase/README.md @@ -1,13 +1,12 @@ # couchbase -[Couchbase Server](https://www.couchbase.com), single node. - -Uses `docker-compose.yml`, publishes ports, and sets `container_name: db` — -unlike the platform-managed services described in the -[root README](../README.md). +[Couchbase Server](https://www.couchbase.com), single node, defined in +`docker-compose.yml` with `container_name: db`. ## Networking +Publishes its ports on the host: + | Ports | Purpose | | --- | --- | | `8091-8097` | Cluster manager, views, query, search, analytics, eventing | diff --git a/diun/README.md b/diun/README.md index 35819aa..11734f4 100644 --- a/diun/README.md +++ b/diun/README.md @@ -21,8 +21,8 @@ So the socket is mounted into instead, and Diun reaches it over the compose network at `tcp://dockerproxy:2375`. `POST` is revoked by default in that image, so container create, `exec`, start and kill return 403. A compromised Diun image -can no longer become root on the host — which matters because Diun is the one -service here whose whole job is to talk to the daemon. +can no longer become root on the host — which matters because Diun's whole job +is to talk to the daemon. Exactly two API sections are granted, both verified against a live watch cycle: @@ -67,7 +67,6 @@ are stale leftovers from an older one — so there is no moving major tag to follow and `latest` is the closest equivalent. Moving tags mean an image can change under a redeploy without this file -changing. That is the accepted trade-off: Diun is the one service here holding -Docker API access, and the proxy is what bounds the damage a bad image could do -— `POST` is revoked, so no image pulled through either tag can create a -privileged container. +changing. That is the accepted trade-off: the proxy is what bounds the damage a +bad image could do — `POST` is revoked, so no image pulled through either tag +can create a privileged container. diff --git a/gitea-mirror/README.md b/gitea-mirror/README.md index 59d929c..709f6a8 100644 --- a/gitea-mirror/README.md +++ b/gitea-mirror/README.md @@ -4,10 +4,8 @@ Self-hosted [Gitea](https://about.gitea.com/) backed by PostgreSQL, with [gitea-mirror](https://github.com/RayLabsHQ/gitea-mirror) mirroring GitHub repositories into it. -Publishes ports, unlike the platform-managed services described in the -[root README](../README.md) — but binds them all to `127.0.0.1`, so nothing is -reachable from outside the host. Put a reverse proxy in front for remote -access. +Every published port binds to `127.0.0.1`, so nothing is reachable from +outside the host. Put a reverse proxy in front for remote access. ## Services @@ -18,13 +16,11 @@ access. | `gitea-mirror` | `ghcr.io/raylabshq/gitea-mirror:latest` | `127.0.0.1:4321` | `gitea` waits for `db` to pass its health check before starting. +`gitea-mirror` sets `pull_policy: always`, so every recreate takes the newest +`latest`. ## Usage -```sh -docker compose up -d -``` - Complete Gitea's first-run setup at , then configure mirroring at . @@ -37,9 +33,8 @@ git clone ssh://git@127.0.0.1:2222//.git ## Configuration `compose.yml` hardcodes everything — database credentials, ports and the Gitea -SSH port are written inline and read no environment variables. `.env.example` -is not wired up: editing a `.env` has no effect until the compose file consumes -it via `env_file:` or `${VAR}` substitution. +SSH port are written inline, and it reads no environment variables, so +`.env.example` is not wired up. The Postgres credentials are `gitea` / `gitea`. Change them before exposing this stack beyond localhost. @@ -51,5 +46,3 @@ this stack beyond localhost. | `db-data` | PostgreSQL data | | `gitea-data` | Repositories, Gitea config and state | | `gitea-mirror-data` | Mirror job database | - -`docker compose down -v` deletes all three. diff --git a/goclaw/README.md b/goclaw/README.md index 26057d2..7ec2f3c 100644 --- a/goclaw/README.md +++ b/goclaw/README.md @@ -10,15 +10,14 @@ sessions, encrypted provider keys and the semantic memory. ## Setup -1. Generate the two secrets and set them, with the rest of `.env.example`, in - Coolify or Dokploy: +1. Generate the two secrets: ```sh openssl rand -hex 16 # GOCLAW_GATEWAY_TOKEN openssl rand -hex 32 # GOCLAW_ENCRYPTION_KEY ``` -2. Point the domain at port `18790` and deploy. Migrations run on start. +2. Map the domain to port `18790` and deploy. Migrations run on start. 3. Open the domain and use the setup wizard to add an LLM provider key. Health check: `GET /health`. @@ -35,8 +34,8 @@ domain would publish the gateway. (AES-256-GCM). It is required for the same reason, and it must not change once keys are stored — every one of them becomes unreadable. -Both are 1:1 with what `prepare-env.sh` generates upstream; that script is not -used here because the platform owns the environment. +Both are the two values upstream's `prepare-env.sh` generates; the script +itself is not used here. ## Environment @@ -79,8 +78,7 @@ credentials survive a recreate. ## Networking -Listens on `18790`, published nowhere — the platform maps the domain to it. See -the [root README](../README.md) for why. +Listens on `18790`. `extra_hosts` maps `host.docker.internal` to the host gateway, so an agent can reach a service running on the host itself. @@ -106,4 +104,4 @@ ships them. The three capabilities are what the entrypoint needs to install persisted packages as root and then drop to the `goclaw` user via `su-exec`. This is a service whose whole purpose is running model-chosen tools, so the -limits are worth keeping even though nothing else here sets them. +limits are worth keeping. diff --git a/opencode/README.md b/opencode/README.md index 9dfb0a0..7b6ebbc 100644 --- a/opencode/README.md +++ b/opencode/README.md @@ -11,20 +11,19 @@ apk layer puts them back. ## Setup -1. Set the variables from `.env.example` in Coolify or Dokploy. -2. Point the domain at port `4096` and deploy. -3. Log in to a model provider — the UI cannot do it, so use a shell: +Logging in to a model provider is the one step the UI cannot do. After the +first deploy, from a shell: - ```sh - docker compose exec opencode opencode auth login - ``` +```sh +docker compose exec opencode opencode auth login +``` - It is interactive, which is why it is not an environment variable; `exec` - gives it the TTY it needs. The credentials land on the `opencode-home` - volume and survive a redeploy. +It is interactive, which is why it is not an environment variable; `exec` +gives it the TTY it needs. The credentials land on the `opencode-home` volume +and survive a redeploy. -4. Open the domain and sign in with `OPENCODE_SERVER_USERNAME` and - `OPENCODE_SERVER_PASSWORD`. +Then open the domain and sign in with `OPENCODE_SERVER_USERNAME` and +`OPENCODE_SERVER_PASSWORD`. The container logs a Bun stack trace ending in `Executable not found in $PATH: "xdg-open"` on every start. `opencode web` @@ -41,14 +40,6 @@ set, the server will be unsecured."* Unset, every request is served — and ever request can ask the agent to run a command. Treat a blank value as publishing a root terminal. -`--hostname 0.0.0.0` in `command:` is what makes the service reachable at all; -both `web` and `serve` bind `127.0.0.1` by default. `--port 4096` is there -because the default is `0`, a random port, which the platform cannot map a -domain to. - -If the UI is loaded from a different origin than it is served from, add -`--cors ` to `command:`. The default setup does not need it. - ## Environment | Variable | Purpose | @@ -57,10 +48,6 @@ If the UI is loaded from a different origin than it is served from, add | `OPENCODE_SERVER_USERNAME` | Username to go with it. opencode falls back to `opencode`. | | `GIT_NAME` / `GIT_EMAIL` | Git author and committer identity for the agent's commits. | -`TZ` is set in `compose.yml` rather than here: it is a property of this setup, -not of whoever deploys it, and Compose interpolation would let a `TZ` exported -by the deploying shell win over the `.env` file anyway. - Model provider credentials are not variables — see [Setup](#setup). ## Storage @@ -79,13 +66,20 @@ anything else installed into `$HOME` persist as a side effect. The container runs as root, which is what the upstream image does; `$HOME` is `/root` because of it. -Anything written outside those two volumes — an `apk add` from the agent's own -terminal — is lost on the next deploy. Add it to the `Dockerfile` instead. +`WORKDIR /workspace` in the `Dockerfile` is what makes the agent start in the +workspace volume. ## Networking -Listens on `4096`, published nowhere — the platform maps the domain to it. See -the [root README](../README.md) for why. +Listens on `4096`; point the domain at it. + +`--hostname 0.0.0.0` in `command:` is what makes the service reachable at all; +both `web` and `serve` bind `127.0.0.1` by default. `--port 4096` is there +because the default is `0`, a random port, which the platform cannot map a +domain to. + +If the UI is loaded from a different origin than it is served from, add +`--cors ` to `command:`. The default setup does not need it. ## Image @@ -96,12 +90,9 @@ longer serves anonymous pulls, so it is not the one to use. The apk layer adds `bash`, `git`, `curl` and `openssh-client` — the floor for an agent that clones, commits and fetches. It carries no language toolchain: none is wanted often enough to justify rebuilding the image for everybody, and -`apk add` from a terminal covers a one-off. Something needed on every deploy -belongs in the `Dockerfile`, since `/usr` is not on a volume. +`apk add` from the agent's own terminal covers a one-off — but `/usr` is not on +a volume, so it is gone on the next deploy. Something needed every time belongs +in the `Dockerfile`. `ENTRYPOINT` stays the image's own `opencode`, so `command:` in `compose.yml` is just the subcommand and its flags. - -## Related - -- [paseo](../paseo/README.md) — runs the opencode CLI, among others, in a terminal diff --git a/paseo/README.md b/paseo/README.md index d8897b2..23d156e 100644 --- a/paseo/README.md +++ b/paseo/README.md @@ -9,16 +9,15 @@ start, see [Agents](#agents). ## Setup -1. Set the variables from `.env.example` in Coolify or Dokploy. -2. Point the domain at port `6767` and deploy. -3. Open the domain. At the pairing screen enter the host **with the port**, +1. Point the domain at port `6767` and deploy. +2. Open the domain. At the pairing screen enter the host **with the port**, then the `PASEO_PASSWORD` value: ``` paseo.example.com:443 ``` -4. List the agents you want in `AGENTS`; the first start installs them. +3. List the agents you want in `AGENTS`; the first start installs them. Log in to each — see [Agents](#agents) — plus `gh auth login` and `glab auth login`. @@ -26,10 +25,8 @@ The port must be typed by hand. The UI rejects a bare hostname, and the auto-connect hint does not help: the daemon builds it from the `Host` header, browsers drop the default `:443`, and the UI discards a hint with no port. What you see instead is its `localhost:6767` placeholder, which in a browser means -your own machine. - -If it stays on `localhost:6767` after you enter the address, clear site data — -the old entry is cached in `localStorage`. +your own machine. If it stays on `localhost:6767` after you enter the address, +clear site data — the old entry is cached in `localStorage`. ## Environment @@ -42,36 +39,6 @@ the old entry is cached in `localStorage`. | `SERVICE_HOSTNAME` | Container hostname, shown as the host label in the UI and in the shell prompt. Without it the label is a random container ID. | | `GIT_NAME` / `GIT_EMAIL` | Git author and committer identity for agents and terminals. | -`SERVICE_HOSTNAME` is used twice: as the container's `hostname:` and as the -`HOST` variable inside it. Coolify injects `HOST=0.0.0.0` into every compose -app, and zsh seeds `$HOST` and the `%m`/`%M` prompt escapes from that variable -rather than calling `gethostname()` -- so the prompt reads `0`, the first -dot-separated field of `0.0.0.0`. Paseo itself never reads `HOST` -- it binds -`PASEO_LISTEN` -- so overriding it only affects the prompt. bash is -unaffected; its `\h` uses the real hostname. - -It is not called `HOSTNAME`, the obvious name, because Compose interpolation -lets the deploying shell's environment win over the `.env` file, and `HOSTNAME` -is set in every container -- including the one Coolify itself runs in. The -container would silently take Coolify's hostname instead of this value. - -Neither name is a Paseo variable: the daemon reads neither `SERVICE_HOSTNAME` -nor `HOST`, and takes the host label from the container hostname. - -`SHELL` and `TZ` hit the same trap, which is why neither is in the table above -or in `.env.example`: they are written into `compose.yml` directly, the way the -`code-server` services do it. `SHELL` is the worse of the two, since every -interactive shell exports it -- a `docker compose up` from a terminal, the way -you would test this locally, would hand Paseo's terminals the *host's* shell. -Harmless when that is bash, which the image has; fatal to every terminal when -it is a path the image lacks. A UTC host would override `TZ` the same way. Both -are properties of this setup rather than of whoever deploys it, so there is -nothing to fill in per deployment. - -Paseo reads `$SHELL` for its terminals and falls back to `/bin/sh` (dash) -otherwise; it ignores the user's login shell, so `chsh` has no effect. That is -what pins it to `/bin/zsh` here. - `PASEO_TRUSTED_PROXIES` matches the proxy's *source IP*, so hostnames are rejected. The daemon trusts `X-Forwarded-Proto` from loopback only, but Coolify's Traefik reaches it from the Docker bridge network — so it reads the @@ -80,23 +47,29 @@ the browser blocks that as mixed content. `uniquelocal` covers the private ranges Docker uses. An exact CIDR works too, but Coolify assigns a fresh subnet per project. -## Networking +`SERVICE_HOSTNAME` is used twice: as the container's `hostname:`, which is +where the daemon takes its host label from, and as the `HOST` variable inside +it. Coolify injects `HOST=0.0.0.0` into every compose app, and zsh seeds `$HOST` +and the `%m`/`%M` prompt escapes from that variable rather than calling +`gethostname()` — so the prompt would read `0`, the first dot-separated field +of `0.0.0.0`. Paseo itself never reads `HOST` — it binds `PASEO_LISTEN` — so +overriding it only affects the prompt. bash is unaffected; its `\h` uses the +real hostname. -Listens on `6767`, published nowhere — the platform maps the domain to it, so -`localhost:6767` on the host refuses connections. See the -[root README](../README.md) for why. +`SHELL=/bin/zsh` and `TZ=Asia/Ho_Chi_Minh` are written into `compose.yml` +directly rather than read from `.env`, which is why neither is in the table. +They are properties of this setup, not of whoever deploys it — and every +interactive shell exports `SHELL`, so a value routed through `.env` could be +replaced by the deploying terminal's own shell, handing Paseo's terminals a +path the image lacks. Paseo reads `$SHELL` for its terminals and falls back to +`/bin/sh` (dash) otherwise; it ignores the user's login shell, so `chsh` has no +effect. That is what pins zsh here. ## Agents The image installs none of them. `entrypoint.sh` does, on start, for every name -in `AGENTS` whose command does not already run: - -``` -AGENTS=claude codex -``` - -That is the default in `.env.example`. The other four are opt-in — add their -names to install them too. +in `AGENTS` whose command does not already run. The default is +`AGENTS=claude codex`; the other four are opt-in. | Agent | Name in `AGENTS` | Installer it runs | Log in with | | --- | --- | --- | --- | @@ -109,57 +82,46 @@ names to install them too. Each vendor's own installer, run as `paseo` through `gosu`, landing in `$HOME` — `~/.local/bin` for most, `~/.opencode/bin` for opencode. `$HOME` is the -`paseo-home` volume, so the binaries and the logins both survive a redeploy, -and the check at the top of each start is all that runs from then on. - -Budget for the first start with a fresh volume: these are fat static binaries, -a few hundred MB each — Claude Code is around 216 MB, `omp` around 208 MB — so -even the default two hold the daemon back by a minute or more, and all six by -several. Nothing is wrong; it is downloading. Later starts skip everything -already present. An agent that fails to install is logged and skipped rather -than taking the container with it, so a bad release or a network blip cannot -leave you without a shell. - -The check is whether the command *runs*, not whether the file exists: the start -asks it for `--version` and reinstalls only on the two exit codes a shell uses -for a binary it could not execute. A `curl | bash` cut short — by a network -drop, or by the platform stopping the container mid-download — leaves a -truncated binary on the volume, and a file-existence check would then skip the -reinstall on every later start while the daemon advertised an agent that fails -on every invocation. An agent that runs but does not understand `--version` -exits with some other code and counts as present, so nothing assumes all six -support the flag. - -An unrecognised name is logged and skipped too. To install one by hand instead, -run its installer in a terminal inside Paseo as `paseo` — never under `sudo`, -where they target root's home and land outside the volume, and where Claude -Code's refuses outright. - -Both directories are on the image's `PATH`, so Paseo picks an agent up as soon -as its installer finishes — no redeploy, no daemon restart. The shell rc entry -the installers add only reaches interactive terminals; the daemon looks the -binary up in its own environment, and without these entries it reports every -self-installed agent as unavailable while a terminal runs it fine. - -The installers pull in any runtime they need, into `$HOME` as well. `omp` is -the one to know about: it is Bun-compiled, and with no Bun on `PATH` its -installer takes the prebuilt binary. Ask for the source build (`--source`) and -it installs Bun to `~/.bun` first. - -Why on start and not in the `Dockerfile`. Two reasons. `paseo` cannot write -`/usr/local`, so a CLI installed there can never apply its own update — every -one of these ships an updater (`claude update`, `codex update`, `omp update`, -…) that expects to rewrite its own binary, and it fails on permissions; Claude -Code nags about it at startup. And a build-time install into `/home/paseo` -would only ever reach a *new* volume: Docker seeds a named volume from the -image once, at creation, and never again. Rebuild with a seventh agent added -and nobody with an existing `paseo-home` would get it. The start-time check has -neither problem — it fires on every fresh volume, and the agents update -themselves in place afterwards. - +`paseo-home` volume, so the binaries and the logins both survive a redeploy. Pi and Oh My Pi are separate projects sharing an ancestor; their commands do not collide. +**First start.** These are fat static binaries, a few hundred MB each — Claude +Code around 216 MB, `omp` around 208 MB — so the default two hold the daemon +back by a minute or more, and all six by several. Later starts skip everything +already present. An agent that fails to install, or a name that is not in the +table, is logged and skipped rather than taking the container with it, so a bad +release or a network blip cannot leave you without a shell. + +**Presence check.** The start asks each command for `--version` and reinstalls +only on exit codes 126 and 127, the shell's own codes for a binary it could not +execute. A `curl | bash` cut short — a network drop, or the platform stopping +the container mid-download — leaves a truncated binary on the volume, which a +file-existence check would accept on every later start while the daemon +advertised an agent that fails on every call. An agent that runs but does not +understand `--version` exits with some other code and counts as present, so +nothing assumes all six support the flag. + +**Installing by hand.** Run the installer in a terminal inside Paseo as +`paseo` — never under `sudo`, where it targets root's home and lands outside +the volume, and where Claude Code's refuses outright. Both install directories +are on the image's `PATH`, so the daemon picks an agent up as soon as its +installer finishes — no redeploy, no restart. The installers pull in any +runtime they need, into `$HOME` as well. `omp` is the one to know about: it is +Bun-compiled, and with no Bun on `PATH` its installer takes the prebuilt +binary. Ask for the source build (`--source`) and it installs Bun to `~/.bun` +first. + +**Why on start and not in the `Dockerfile`.** `paseo` cannot write +`/usr/local`, so a CLI installed there can never apply its own update — every +one of these ships an updater (`claude update`, `codex update`, `omp update`, +…) that rewrites its own binary, and it fails on permissions; Claude Code nags +about it at startup. And a build-time install into `/home/paseo` would only +ever reach a *new* volume: Docker seeds a named volume from the image once, at +creation, and never again. Rebuild with a seventh agent added and nobody with +an existing `paseo-home` would get it. The start-time check fires on every +fresh volume, and the agents update themselves in place afterwards. + ## Storage | Volume | Mount | Holds | @@ -167,11 +129,10 @@ not collide. | `paseo-home` | `/home/paseo` | Daemon state, agent CLIs and their configs and credentials (`.claude`, `.codex`, `.config/*`) | | `paseo-workspace` | `/workspace` | Code the agents work on | -The agent CLIs, `gh` and `glab` all keep their config under `/home/paseo`, so -every login survives a redeploy — the base image points `CLAUDE_CONFIG_DIR`, -`CODEX_HOME` and the `XDG_*` variables into it. The agent binaries live there -too, since you install them yourself. Dotfiles as well, so a `.zshrc` or -oh-my-zsh install persists. Anything written outside `$HOME` (`chsh`, +The agent CLIs, `gh` and `glab` all keep their config under `/home/paseo` — the +base image points `CLAUDE_CONFIG_DIR`, `CODEX_HOME` and the `XDG_*` variables +into it — so every login survives a redeploy, as do dotfiles such as a `.zshrc` +or an oh-my-zsh install. Anything written outside `$HOME` (`chsh`, `apt install`) is lost on rebuild. ## Image @@ -181,67 +142,59 @@ oh-my-zsh install persists. Anything written outside `$HOME` (`chsh`, | `gh` | GitHub's signed apt repo | Debian does not package it | | `glab` (`GLAB_VERSION`) | The `.deb` on GitLab's releases page | Debian does not package it, and GitLab runs no apt repo | | Python (`PYTHON_VERSION`) | `uv python install` | Debian 12 ships 3.11 | -| `build-essential`, `sudo` | apt | — | -| `zsh`, `nano` | apt | — | +| `build-essential`, `sudo`, `zsh`, `nano` | apt | — | Bump a pinned version with a build arg, e.g. `--build-arg PYTHON_VERSION=3.13`. `uv` itself is installed too. `GLAB_VERSION` is pinned rather than tracking the latest because GitLab's download URL carries the version in the path. -The `Dockerfile` itself only says what each layer installs. The reasoning is -all here: +The `Dockerfile` and `entrypoint.sh` only say what each step does. The +reasoning: -- `$HOME` is `/home/paseo`, a volume that masks anything the build writes - there. Hence `/opt/python` rather than the default. -- Do not add agent CLIs here, or a runtime only they need. Under `/usr/local` - they cannot self-update; in `$HOME` they can, and they persist anyway. `omp` - needs Bun for its source build, and that belongs in `$HOME` for the same - reason. See [Agents](#agents). -- One concern per layer, cheapest and least-changing first, so bumping a - version rebuilds as little as possible. -- `build-essential` is the C toolchain the language layers assume but do not - ship: npm's node-gyp addons and Python C extensions both shell out to `gcc` - and `make`. It rides along in the apt layer so there is one `apt-get update`. -- No other language toolchain is in the image, because none is wanted often - enough to pay for a rebuild. Install Go, a JVM or anything else into `$HOME` - from a terminal, where it persists on the `paseo-home` volume like the agent - CLIs do. +- Python lives in `/opt/python` rather than under `$HOME`, the `uv` default, + because `/home/paseo` is a volume that masks anything the build writes there. +- No agent CLIs, and no runtime only they need (`omp`'s Bun). Under + `/usr/local` they cannot self-update; in `$HOME` they can, and they persist + anyway. See [Agents](#agents). +- `build-essential` is the C toolchain npm's node-gyp addons and Python C + extensions shell out to for `gcc` and `make`. No other language toolchain is + in the image, because none is wanted often enough to pay for a rebuild — + install Go, a JVM or anything else into `$HOME` from a terminal, where it + persists on the `paseo-home` volume like the agent CLIs do. - `git` and `curl` are already in the base image. `sudo` is not, despite Debian's `base-passwd` shipping an empty `sudo` group, so the apt layer adds it and puts `paseo` in the group. -- The image stays root: `entrypoint.sh` does its work, then hands over to the - base entrypoint, which drops to the `paseo` user (uid 1000) with `gosu`. -- `entrypoint.sh` is installed as `/usr/local/bin/entrypoint`, next to the - base image's `paseo-docker-entrypoint`, which it wraps. -- `entrypoint.sh` runs before the base entrypoint, not after: that one ends in - `exec gosu paseo` and never returns, and by then is no longer root. Every - job it has needs root — `chpasswd`, and `gosu paseo` for the agent - installs. -- It sets the `paseo` password on every start rather than at build, so the - password never lands in an image layer, and because `/etc/shadow` is in the - image rather than on a volume and reverts on each recreate. The password is - piped, not passed as an argument, since arguments are visible in `ps`; - `chpasswd` splits on the first colon, so a colon in the password is fine. A - failure there is logged and the start continues, because `chpasswd` rejects a - multi-line value and under `set -e` that would otherwise take the whole - service down rather than just the password. An empty `PASEO_PASSWORD` leaves - the account locked and `sudo` unusable. -- It does not `chown` `/home/paseo` before the agent installs. The base image - declares `/home/paseo` a volume and ships it owned by uid 1000, so a fresh - named volume is seeded with that ownership; the base entrypoint also chowns - it, along with `PASEO_HOME`, `CLAUDE_CONFIG_DIR`, `CODEX_HOME` and the XDG - directories, whenever one of them is owned by root. -- `chpasswd` and `gosu` are called by absolute path. The agent `PATH` - entries come first in the image's `PATH`, including root's, and they live on - a volume that anything running as `paseo` — an agent, by design — can write - to; a file planted there under one of those names would otherwise run as root - on the next start. -- Globbing is off around the `AGENTS` loop. The list is split unquoted, so - a `*` in it would otherwise expand against `/workspace`, the working - directory, and report every file in it as an unknown agent. -- The agent `PATH` entries belong in the image, not in a shell rc: the daemon - probes for each provider's binary with `which` in its own environment, which - comes from the image and never sources an rc file. They are spelled - `/home/paseo/...` because `ENV` only expands variables the `Dockerfile` itself - set earlier. +- The image stays root. `entrypoint.sh`, installed as + `/usr/local/bin/entrypoint`, runs *before* the base image's + `paseo-docker-entrypoint` and then hands over to it: that one ends in + `exec gosu paseo` and never returns, and every job here — `chpasswd`, and + `gosu paseo` for the agent installs — needs root. +- The `paseo` password is set on every start rather than at build, so it never + lands in an image layer, and because `/etc/shadow` is in the image rather + than on a volume and reverts on each recreate. It is piped, not passed as an + argument, since arguments are visible in `ps`; `chpasswd` splits on the first + colon, so a colon in the password is fine. A failure there is logged and the + start continues, because `chpasswd` rejects a multi-line value and under + `set -e` that would otherwise take the whole service down rather than just + the password. An empty `PASEO_PASSWORD` leaves the account locked and `sudo` + unusable. +- `/home/paseo` is not `chown`ed before the agent installs. The base image + declares it a volume and ships it owned by uid 1000, so a fresh named volume + is seeded with that ownership; the base entrypoint also chowns it, along + with `PASEO_HOME`, `CLAUDE_CONFIG_DIR`, `CODEX_HOME` and the XDG directories, + whenever one of them is owned by root. +- `chpasswd` and `gosu` are called by absolute path. The agent `PATH` entries + come first in the image's `PATH`, including root's, and they live on a + volume that anything running as `paseo` — an agent, by design — can write to; + a file planted there under one of those names would otherwise run as root on + the next start. +- Globbing is off (`set -f`) around the `AGENTS` loop. The list is split + unquoted, so a `*` in it would otherwise expand against `/workspace`, the + working directory, and report every file in it as an unknown agent. +- The agent `PATH` entries are `ENV` in the image, not a shell rc entry: the + daemon probes for each provider's binary with `which` in its own environment, + which never sources an rc file, and would otherwise report every + self-installed agent as unavailable while a terminal ran it fine. They are + spelled `/home/paseo/...` because `ENV` only expands variables the + `Dockerfile` itself set earlier. diff --git a/traffmonetizer/README.md b/traffmonetizer/README.md index 083c8ff..57451be 100644 --- a/traffmonetizer/README.md +++ b/traffmonetizer/README.md @@ -1,11 +1,8 @@ # traffmonetizer -[TraffMonetizer](https://traffmonetizer.com) bandwidth-sharing client. - -Uses `docker-compose.yml`, and sets `container_name: tm` and `restart: always` -rather than the `unless-stopped` everything else uses — unlike the -platform-managed services described in the [root README](../README.md). Publishes no ports; the client only makes outbound -connections. +[TraffMonetizer](https://traffmonetizer.com) bandwidth-sharing client, defined +in `docker-compose.yml` with `container_name: tm` and `restart: always`. It +only makes outbound connections, so there is no port to map a domain to. The image tag is `arm64v8`. Change it to match the host architecture.