-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathcompose.example.yml
More file actions
257 lines (254 loc) · 13.4 KB
/
Copy pathcompose.example.yml
File metadata and controls
257 lines (254 loc) · 13.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
# docker/compose.example.yml
#
# Example Compose file for running `remo web` in a home lab (010-web-session-
# interface, US4). Copy/adapt this file — it is not used automatically.
#
# This file is self-contained: it pulls a published image, so it is the only
# file you need. Everything it references from the repo is either a comment
# or an optional build override.
#
# See specs/010-web-session-interface/quickstart.md "B. Container run" and
# research.md R8 for the full rationale behind these hardening defaults.
services:
remo-web:
# Published on each stable release by .github/workflows/release.yml, as a
# multi-arch manifest (linux/amd64 + linux/arm64) — Docker selects the
# right one for your host, so a Pi and an x86 box use the same line.
#
# `latest` only ever tracks stable releases (pre-release tags are never
# published as latest). Pin a specific version — e.g. :2.1.0 — if you'd
# rather upgrade deliberately than on every `docker compose pull`.
image: ghcr.io/get2knowio/remo-web:latest
# To build from a source checkout instead of pulling (local development,
# or to run an unreleased change), comment out `image:` above and
# uncomment this — it is the same Dockerfile the published image is built
# from. Requires the full repo, and this file must stay at docker/ for the
# relative context to resolve.
# build:
# context: ..
# dockerfile: docker/Dockerfile
ports:
# Bind to loopback only by default — this is a LAN-trust-boundary
# service (FR-052), not meant to be exposed directly to the internet.
# Change to "0.0.0.0:8080:8080" (or a specific LAN IP) deliberately if
# you want other hosts on your LAN to reach it.
- "127.0.0.1:8080:8080"
# Deployment-specific env vars: set these to your actual LAN hostname/IP
# or tailnet name so the browser can reach the service (FR-048 — no
# wildcard host/origin allowlisting, ever). Uncomment `environment:` and
# adapt the values below if you need to override the localhost-only
# defaults.
# environment:
# - "REMO_WEB_ALLOWED_HOSTS=remo-web.lan,127.0.0.1,localhost"
# - "REMO_WEB_ALLOWED_ORIGINS=http://remo-web.lan:8080,http://127.0.0.1:8080"
#
# NOTE: REMO_WEB_FRONTEND_DIST_DIR is already baked into the image via
# `ENV` in docker/Dockerfile (it must match where the built frontend
# assets actually live in this image) — no need to repeat it here.
#
# NOTE: REMO_HOME / the registry path do not need an explicit override
# either — core/config.py resolves the registry under `~/.config/remo`
# by default, and the `remo` user's $HOME is /home/remo inside this
# image, matching the registry volume mount below.
volumes:
# Registry (read-only): host/instance/project discovery data.
# NOTE: this is the registry, NOT SSH auth material (FR-026) — see the
# separate mounts below for that.
- "${HOME}/.config/remo:/home/remo/.config/remo:ro"
# SSH material (read-only): private key, client config, known_hosts.
#
# Every source path below must already EXIST on the host. Docker creates
# a missing bind source as a root-owned empty *directory* rather than
# failing, and ssh then silently ignores a directory-shaped config /
# known_hosts — `remo web check` still passes (it only asserts an
# identity file exists), so the breakage surfaces later as puzzling
# per-instance connection failures. Delete any mount below you don't
# actually have rather than letting Docker fabricate it.
#
# Only the private key is required. Adjust the key path if yours isn't
# id_ed25519 (or point $REMO_WEB_SSH_IDENTITY_FILE at it).
#
# The key must be READABLE BY UID 1000 — the uid this container runs as
# (see `user:` below). Bind mounts keep their host ownership, so if your
# login account isn't uid 1000, the conventional 0600 key you mounted is
# unreadable inside the container and startup fails with "no SSH private
# key found" — which reads like the mount is missing when it is really
# just unreadable. Check with `id -u`; if it isn't 1000, grant uid 1000
# read access to the key, e.g. with a POSIX ACL that leaves the host
# mode 0600 intact:
#
# setfacl -m u:1000:r ~/.ssh/id_ed25519
#
# Note that changing `user:` to your own uid is NOT a workaround: the
# image's /home/remo is mode 0700 owned by uid 1000, so any other uid
# cannot traverse it and every mount below becomes unreachable. Grant
# uid 1000 access to the key instead, or use a dedicated key for this
# service.
- "${HOME}/.ssh/id_ed25519:/home/remo/.ssh/id_ed25519:ro"
# Optional: remo connects to registry entries by explicit user@host and
# never relies on `Host` aliases, so this is only needed if your setup
# depends on extra client config (e.g. ProxyJump to reach an instance).
- "${HOME}/.ssh/config:/home/remo/.ssh/config:ro"
# Host keys for direct-access (incus/proxmox/hetzner) instances, which
# — unlike SSM-routed ones — are subject to strict host key checking.
- "${HOME}/.ssh/known_hosts:/home/remo/.ssh/known_hosts:ro"
# Optional: AWS credentials/profile for SSM-routed targets (SC-006).
# Uncomment and adapt if any registered targets use the SSM proxy path.
# - "${HOME}/.aws:/home/remo/.aws:ro"
tmpfs:
# Per-instance SSH ControlMaster sockets (R5) — must be writable even
# though the root filesystem is read-only.
- "/run/remo-ssh"
read_only: true
user: "1000:1000"
security_opt:
- "no-new-privileges:true"
cap_drop:
- "ALL"
healthcheck:
# Matches GET /api/v1/ready (contracts/rest-api.md) — 200 only when
# registry/SSH identity/runtime dir/executables all check out; 503
# otherwise. `curl` is installed as a runtime dependency in the image
# (docker/Dockerfile) specifically so this healthcheck can run.
test: ["CMD", "curl", "-fsS", "http://127.0.0.1:8080/api/v1/ready"]
interval: 30s
timeout: 5s
retries: 3
start_period: 10s
restart: unless-stopped
# ---------------------------------------------------------------------------
# Adopted mode (011-web-adopt) — an ALTERNATIVE to `remo-web` above; run one
# or the other, not both (they publish the same host port).
#
# When to use which:
#
# - `remo-web` (above, bind-mount mode): you are happy to mount your
# existing registry and SSH material read-only into the container. The
# service boots fully configured with YOUR key; nothing else to do.
# Upgrading to this release changes nothing about that mode (SC-005).
#
# - `remo-web-adopted` (below): nothing from your workstation is mounted.
# The service boots UNCONFIGURED with an empty named state volume,
# generates its own service-scoped SSH identity there on first start
# (under web-identity/, comment `remo-web@<deployment-id>`), and waits
# in a stable "awaiting adoption" state (GET /api/v1/ready -> 200
# `{"status": "unconfigured"}`). Complete setup from a workstation with
# a working remo CLI:
#
# remo web adopt http://<docker-host>:8080
#
# Open http://<docker-host>:8080 in a browser (through your forward-auth
# proxy, see below), click "Copy pairing code", run the command above, and
# paste the code when prompted. It pushes your registry + instance host
# keys and authorizes the service's OWN public key on each instance — your
# personal private key never leaves the workstation. Later registry changes
# are re-pushed with `remo web push` (open the dashboard's "Pair CLI to
# sync" affordance for a fresh code).
#
# Setup authorization (012-web-adopt-pairing): there is NO static token. The
# awaiting-adoption page mints a short-lived, single-use PAIRING CODE; the
# /api/v1/setup/* surface is dormant (404) unless a pairing session is live.
# Minting a code is gated by OPERATOR AUTHENTICATION:
#
# - Recommended: put a forward-auth reverse proxy (Traefik ForwardAuth /
# oauth2-proxy / Authelia / a hola app's SSO) in front. It must (a) gate
# the browser-facing POST /api/v1/pairing/mint so only an authenticated
# operator can mint, while (b) PASSING THROUGH /api/v1/setup/* unchanged
# (the CLI cannot do SSO — it authenticates with the pairing code alone).
# Set REMO_WEB_OPERATOR_AUTH=forward and REMO_WEB_FORWARD_AUTH_HEADER to
# the trusted identity header your proxy injects (e.g. X-Forwarded-User).
# The proxy MUST strip that header from inbound client requests.
#
# - Loopback/dev only: REMO_WEB_OPERATOR_AUTH=none mints WITHOUT operator
# auth (a loud, weaker posture surfaced in readiness). Use only when the
# port is loopback-bound or otherwise network-restricted.
#
# This service carries `profiles: ["adopted"]` so a plain `docker compose
# up` still starts only the bind-mount service above. Start this one with:
#
# docker compose --profile adopted up -d
remo-web-adopted:
profiles: ["adopted"]
# Same image (and the same optional `build:` override) as `remo-web`
# above — the mode is chosen purely by what is mounted, not by the image.
image: ghcr.io/get2knowio/remo-web:latest
ports:
# Adoption traffic (the pairing-code-authenticated /api/v1/setup/* calls
# made by `remo web adopt`) arrives over this same port. Loopback-only
# means the workstation running the CLI must be the Docker host itself —
# otherwise bind a LAN IP here deliberately (behind your forward-auth
# proxy), or keep loopback and run `remo web adopt --via <docker-host>`
# to tunnel over SSH.
- "127.0.0.1:8080:8080"
environment:
# Operator-authentication posture for pairing-code minting (see the notes
# above). Choose ONE:
#
# Forward auth (recommended, behind an SSO proxy):
# - "REMO_WEB_OPERATOR_AUTH=forward"
# - "REMO_WEB_FORWARD_AUTH_HEADER=X-Forwarded-User"
# (Enabling forward auth without a header name is a fail-fast error.)
#
# Network-restricted (loopback/dev only — weaker, loudly logged):
- "REMO_WEB_OPERATOR_AUTH=none"
#
# There is NO REMO_WEB_API_TOKEN anymore — a value set for it is ignored.
# The REMO_WEB_ALLOWED_HOSTS / REMO_WEB_ALLOWED_ORIGINS notes on the
# service above apply here unchanged (the mint endpoint needs the page's
# Origin allowlisted; the default 127.0.0.1:8080 origin covers loopback).
volumes:
# A writable named state volume at REMO_HOME — the ONLY volume in this
# mode (no registry mount, no ~/.ssh mounts; presence of those is what
# puts the service in mount-configured mode instead). It holds the
# pushed registry, per-instance host keys, and the service identity
# keypair. Deleting the volume resets the service to unconfigured with
# a NEW identity on next boot; it revokes nothing on the instances —
# remove the old `remo-web@<deployment-id>` authorized_keys line there.
- "remo-web-state:/home/remo/.config/remo"
# A PLAIN, option-less tmpfs — no `rw,mode=1777` pin needed anymore. The
# image starts as root and its entrypoint re-heals ownership on
# /run/remo-ssh on EVERY start, so the well-known Docker quirk (an
# option-less tmpfs is remounted root-owned 0755 by a container *restart*)
# no longer breaks the startup runtime-dir check on the second boot.
tmpfs:
- "/run/remo-ssh"
read_only: true
# NOTE: no `user:` pin here (unlike the bind-mount service above). This
# service starts as ROOT so the entrypoint can self-heal ownership on the
# state volume (Docker seeds a fresh named volume root-owned; a bind-mount
# host dir is root-owned too) before dropping — via gosu — to the
# unprivileged `remo` user. The app STILL runs as non-root `remo`; only
# the brief healing pass runs as root. See docker/entrypoint.sh.
security_opt:
- "no-new-privileges:true"
# Drop everything, then add back ONLY the capabilities the heal-then-drop
# entrypoint needs while it is root: CHOWN (fix ownership of the state
# dir/tmpfs), DAC_OVERRIDE (traverse/write a dir owned by another uid),
# FOWNER (defensive — chmod/utime on files owned by another uid),
# SETUID+SETGID (gosu drops to the `remo` uid).
# The dropped-to `remo` process holds NONE of these effectively — dropping
# uid clears the permitted/effective sets — so the security posture is
# unchanged from a plain non-root run; `no-new-privileges` does not block
# gosu (dropping privileges is always allowed).
cap_drop:
- "ALL"
cap_add:
- "CHOWN"
- "DAC_OVERRIDE"
- "FOWNER"
- "SETUID"
- "SETGID"
healthcheck:
# /api/v1/ready answers 200 for BOTH `ready` and `unconfigured` — a
# container awaiting adoption is healthy and must not crash-loop or
# flap the healthcheck while it waits (SC-006).
test: ["CMD", "curl", "-fsS", "http://127.0.0.1:8080/api/v1/ready"]
interval: 30s
timeout: 5s
retries: 3
start_period: 10s
restart: unless-stopped
volumes:
# Adopted-mode state (see `remo-web-adopted`). Created on first
# `--profile adopted` start; harmless if you only use bind-mount mode.
remo-web-state: