-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathdocker-compose.yml
More file actions
143 lines (141 loc) · 7.44 KB
/
Copy pathdocker-compose.yml
File metadata and controls
143 lines (141 loc) · 7.44 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
services:
app:
build:
context: .
dockerfile: Dockerfile
ports:
- "${PORT:-3001}:3001"
volumes:
- ${WORKSPACE_ROOT:-/var/shared-terminal/workspaces}:/var/shared-terminal/workspaces
- /var/run/docker.sock:/var/run/docker.sock
environment:
- PORT=3001
# Marks this as a production deploy so validateJwtSecret()
# treats the insecure-default signing key as a hard error.
# Trust model: explicitly overriding this (NODE_ENV=development
# docker compose up) downgrades the placeholder check from throw
# to warn. The `:?` check on JWT_SECRET below still covers the
# accidental "forgot to set it entirely" case, so the weakened
# path is only reachable by deliberate action.
- NODE_ENV=${NODE_ENV:-production}
# `:?msg` form makes `docker compose up` fail fast if JWT_SECRET
# isn't set in .env instead of booting with a known-public default.
- JWT_SECRET=${JWT_SECRET:?JWT_SECRET must be set in .env (see .env.example)}
- JWT_EXPIRES_IN=${JWT_EXPIRES_IN:-7d}
# `SECRETS_ENCRYPTION_KEY` is required for #186's AES-256-GCM
# secret-entry path; the backend `validateSecretsKey()` boot
# check throws on a missing/malformed key and the container
# crash-loops. The `:?` guard here surfaces the failure to
# `docker compose up` directly so the operator sees it without
# tailing container logs. Generate with: openssl rand -base64 32
# Single-quoted so YAML doesn't try to parse the `with: openssl`
# colon-space inside the `:?` error message as a map key. The
# JWT_SECRET line above gets away without quotes because its
# error message has no `key: value`-shaped fragment.
- 'SECRETS_ENCRYPTION_KEY=${SECRETS_ENCRYPTION_KEY:?SECRETS_ENCRYPTION_KEY must be set in .env — generate with openssl rand -base64 32 (see .env.example)}'
- CORS_ORIGINS=${CORS_ORIGINS:-*}
# Defaults to "0" (trust nothing) so a direct-connection dev setup
# can't be XFF-spoofed. Set to "1" in your .env when deploying behind
# Cloudflare Tunnel — otherwise the per-IP /auth rate limiter
# collapses every request into the tunnel's shared IP bucket.
- TRUST_PROXY=${TRUST_PROXY:-0}
# Per-session port dispatcher (#190). Pair: setting one
# without the other is a foot-gun the backend warns on at
# boot — `PORT_PROXY_BASE_DOMAIN` enables the dispatcher
# subdomain routing, `COOKIE_DOMAIN` widens the JWT cookie
# scope so the browser actually sends it to the per-session
# `p<port>-<sid>.<base>` URLs. With `COOKIE_DOMAIN` unset
# the cookie stays host-only and `public: false` ports
# 401 the authenticated owner. Both default to empty so a
# local dev `docker compose up` without `.env` entries
# keeps booting (dispatcher self-disables, logged once).
- PORT_PROXY_BASE_DOMAIN=${PORT_PROXY_BASE_DOMAIN:-}
- COOKIE_DOMAIN=${COOKIE_DOMAIN:-}
- SESSION_IMAGE=${SESSION_IMAGE:-shared-terminal-session}
- WORKSPACE_ROOT=/var/shared-terminal/workspaces
# Shared network the dispatcher uses to reach session containers by
# name. Must equal the `sessions-net` network defined below; forwarded
# so DockerManager attaches spawned containers to the same network.
- SESSIONS_NETWORK=${SESSIONS_NETWORK:-sessions-net}
# Operator per-session resource ceilings (#194). Documented in
# .env.example but previously not forwarded here, so setting them
# in .env silently did nothing — the same trap PORT_PROXY_BASE_DOMAIN
# fell into. Empty string is treated as unset by parseMaxSessionCpu /
# parseMaxSessionMem (falls back to the v1 hard ceilings), so the
# `:-` defaults keep an .env-less dev boot byte-identical.
- MAX_SESSION_CPU=${MAX_SESSION_CPU:-}
- MAX_SESSION_MEM=${MAX_SESSION_MEM:-}
- CLOUDFLARE_ACCOUNT_ID=${CLOUDFLARE_ACCOUNT_ID}
- CLOUDFLARE_API_TOKEN=${CLOUDFLARE_API_TOKEN}
- D1_DATABASE_ID=${D1_DATABASE_ID}
# Web Push (#355). All three unset → push is silently disabled (the
# backend logs one warn at boot and never sends). Public key is served
# to the frontend via GET /api/push/vapid-key so no frontend build-time
# env var is needed. `:-` defaults keep dev/local working without them.
- VAPID_PUBLIC_KEY=${VAPID_PUBLIC_KEY:-}
- VAPID_PRIVATE_KEY=${VAPID_PRIVATE_KEY:-}
- VAPID_SUBJECT=${VAPID_SUBJECT:-}
# Join the shared session network so the backend can reach each session
# container directly at `http://<container_name>:<port>` (Docker embedded
# DNS). Session containers are spawned onto the same network by name.
networks:
- sessions-net
restart: unless-stopped
# #345 — bound the json-file driver. Without a cap, backend logs
# accumulate in /var/lib/docker/containers/<id>/*-json.log until the
# host disk fills. Session containers aren't compose services, so this
# block can't cover them — they get the equivalent cap via LogConfig
# in DockerManager.spawn().
logging:
driver: json-file
options:
max-size: 10m
max-file: "3"
# #346 — `restart: unless-stopped` only fires on process EXIT; a
# wedged-but-alive Node (event-loop stall, stuck outbound calls)
# stayed "running" forever with no signal. /health is a static
# in-process JSON route (backend/src/index.ts), so this probes
# exactly the two things that wedge — the event loop and the HTTP
# stack — and deliberately NOT D1, which has its own timeout and
# whose brown-outs must not read as "backend unhealthy" (that would
# turn an upstream blip into restart churn if an auto-restarter is
# attached). Docker itself only FLAGS unhealthy (docker ps, events);
# pair with an autoheal sidecar or alerting if auto-recycle is
# wanted. node -e fetch avoids a curl/wget dependency in the image.
healthcheck:
test: ["CMD", "node", "-e", "fetch('http://127.0.0.1:3001/health').then((r)=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"]
interval: 30s
timeout: 5s
retries: 3
start_period: 30s
# Build-only helper — the session image needs to exist in Docker's image
# store before `app` can spawn session containers, but we don't want to
# *run* anything from it as a long-lived service. The `session-image`
# Dockerfile has an ENTRYPOINT (tmux + sleep), so starting a container
# here would leave an idle parasite container running forever.
#
# With `profiles: ["build"]` this service is never started by a plain
# `docker compose up`. To (re)build the image, run:
#
# docker compose build session-image
#
# (`docker compose build <name>` works regardless of profile.) Or use
# the standalone `docker build -t shared-terminal-session ./session-image`
# from the repo root — both produce the same image.
session-image:
profiles: ["build"]
build:
context: ./session-image
image: shared-terminal-session
# Shared network for the backend ⋈ session containers. Declared `external` so
# its real Docker name is exactly `sessions-net` (a compose-managed network
# would be prefixed `<project>_sessions-net`, which wouldn't match the
# `SESSIONS_NETWORK` value DockerManager passes as the spawned container's
# NetworkMode). Create it once before `docker compose up`:
#
# docker network create sessions-net
#
networks:
sessions-net:
external: true
name: ${SESSIONS_NETWORK:-sessions-net}