mirror of
https://github.com/tiennm99/composes.git
synced 2026-10-11 03:13:16 +00:00
docs: keep each service README inside its own directory
A service README now describes only its own service: no links to other services or to the root, and no restating of the shared conventions that the root README and CLAUDE.md already carry. Each service is a separate Coolify app on a <service>/** watch path, so a cross-link made editing one service redeploy another. The rule is recorded at the root; the alloy compose comment now points at a heading that exists.
This commit is contained in:
1 parent
daf18abf70
commit
4c66ec76e6
12 files changed
+214
-267
No files matched your search
@@ -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 `<service>/**`. 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 `<service>/**`.
|
||||
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
|
||||
|
||||
@@ -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
|
||||
`<service>/**` watch path.
|
||||
|
||||
## Usage
|
||||
|
||||
In Coolify or Dokploy, point a Docker Compose resource at the service directory
|
||||
|
||||
+7
-5
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
+22
-33
@@ -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.
|
||||
+4
-5
@@ -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 |
|
||||
|
||||
+5
-6
@@ -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.
|
||||
+6
-13
@@ -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 <http://127.0.0.1:3000>, then configure
|
||||
mirroring at <http://127.0.0.1:4321>.
|
||||
|
||||
@@ -37,9 +33,8 @@ git clone ssh://git@127.0.0.1:2222/<owner>/<repo>.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.
|
||||
+6
-8
@@ -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.
|
||||
+24
-33
@@ -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 <url>` 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 <url>` 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
|
||||
+110
-157
@@ -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.
|
||||
@@ -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.
|
||||
|
||||
|
||||
Reference in new issue
Block a user