-
Notifications
You must be signed in to change notification settings - Fork 3
Expand file tree
/
Copy pathdocker-compose.yaml
More file actions
120 lines (118 loc) · 7.67 KB
/
Copy pathdocker-compose.yaml
File metadata and controls
120 lines (118 loc) · 7.67 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
# 47 §1(c) — the minimal compose file **for evaluation**. (sem: SEM-docker-compose.yaml-001)
#
# > **(c) `docker-compose.yaml`** — for evaluation. A `gx serve` container + a named volume for
# > the ledger (persisting `gx-log`'s append-only tile store); for single-machine functional checks, PoCs, and a small customer's first install (sem: SEM-docker-compose.yaml-002)
#
# 47 §6 makes this and the single static binary the **only** two v0.1-required artefacts. There is no
# signed image (47 §1(b)) and no Helm chart (47 §1(d)) in this repository, and req/88 §1 N-10 keeps
# T3 (K8s) and T4 (air-gapped) out of M6 altogether.
#
# ---------------------------------------------------------------------------
# 🔴 The loopback problem, which is the interesting part of this file
# ---------------------------------------------------------------------------
#
# `gx serve` **refuses to bind anywhere but loopback**, without an override flag. That is not an
# oversight; it is §53 M6H6-6, adopted (a): (sem: SEM-docker-compose.yaml-003)
#
# > **M6H6-6, adopted (a), v0.1**: unconditional refusal of any non-loopback bind. (b)
# > `--allow-remote-bind` shares a window with the authorization implementation (sem: SEM-docker-compose.yaml-004)
#
# v0.1 has no authorization layer (M5H6-4, adopted (a)): the Bearer token is one static string and no (sem: SEM-docker-compose.yaml-005)
# request is tied to an `Actor`, so a socket on a routable interface is a socket on which anyone can
# `cancel` somebody else's transformation.
#
# Docker's ordinary `ports:` publishing **cannot work under that rule**. Publishing forwards the
# host's port to the container's network namespace, and a process listening on that namespace's
# `127.0.0.1` is not reachable from it — the container would start, `gx serve` would be running, and
# `curl` would be refused. Two ways out exist and this file takes the first:
#
# 1. **`network_mode: host`** (this file). The container shares the host's network namespace, so
# the loopback `gx serve` binds to *is the evaluator's own loopback* and `127.0.0.1:8080` works
# from the host with the fail-closed rule completely intact. This is exactly 47 §1(c)'s stated
# audience — "single-machine functional checks, PoCs" — and on a single machine host networking is not a (sem: SEM-docker-compose.yaml-006)
# compromise, it is the shape of the deployment.
# Caveat, because it is a real one: host networking is native on Linux and is a **recent and
# optional** feature of Docker Desktop on Windows and macOS. On a Desktop without it, this file
# starts the container and the port is not reachable from the host.
# 2. **M6H6-6 (b)'s `--allow-remote-bind`**, which does not exist in v0.1 and whose window §53 tied
# to the authorization work. A compose that published a port would need it.
#
# 🔴 So 47 §1(c) and M6H6-6 (adopted (a)) are in tension, and this is the first artefact where the tension is
# load-bearing rather than theoretical. req/95 raises it (**M6H7**), because "ship it via compose" and
# "refuse anything but loopback" cannot both be unconditional. Note also that M5H6-4(b)'s firing condition —
# "when HTTP binds anywhere but loopback" — is what a published port *is*: an operator who reaches (sem: SEM-docker-compose.yaml-007)
# for `ports:` here has met the condition, and the refusal is what makes them notice.
#
# ---------------------------------------------------------------------------
# Before `up`: two things this file cannot do for you
# ---------------------------------------------------------------------------
#
# * `docker compose build` needs the measured static binary at
# `target/x86_64-unknown-linux-gnu/release/gx` (see `Dockerfile`, and `tools/m6h7_dist.sh
# static` for the build and its `readelf` evidence).
# * `gx serve` refuses to start without a Bearer token (44 §2.5 makes it "required") and without a signing (sem: SEM-docker-compose.yaml-008)
# key. Create both into the volume once:
#
# docker compose run --rm gx key gen --out /project/.gx/keys/engine.json
# printf 'a-token-you-chose' > ./gx-token
#
# The token is passed as a **path** and not as a value: `--token-file` exists so that the secret
# does not appear in `ps` (M6H6-8 = E-M6-23).
services:
gx:
build:
context: .
dockerfile: Dockerfile
image: glovrex/gx:0.1.0-dev
# See the header. `ports:` is deliberately absent.
network_mode: host
# 44 §1.1's verb, with the flags 44 §1.2 and E-M6-23 give it. `--bind` is the default and is
# written out so that a reader does not have to know what the default is.
command:
- serve
- --bind
- 127.0.0.1:8080
- --token-file
- /run/secrets/gx-token
# 🔴 The engine signing key, by id. E-M6-7 gives `.gx/config.toml` an
# `engine_signing_keyid = "…"` slot and `gx serve` reads it — but **no command writes it**, and
# a `scratch` image has no shell to write it with, so a fresh volume has no way to record the
# value. Passing the id here is the workaround, not the design; req/95 raises the missing
# writer. The id is per deployment (it is derived from the key `gx key gen` drew), so it comes
# from the environment rather than being baked in:
#
# GX_SIGNING_KEY_ID=$(docker compose run --rm gx key gen --json | jq -r .key_id)
# GX_SIGNING_KEY_ID=$GX_SIGNING_KEY_ID docker compose up -d
- --signing-key
- ${GX_SIGNING_KEY_ID:?run `docker compose run --rm gx key gen --json` first and export its key_id}
environment:
# 🔴 req/56 §3 puts private keys in the **user home** — "private key = `~/.gx/keys/` (user home, 0600).
# the project side holds **only a reference to the public keyid**" — and a `scratch` image has no `/etc/passwd`, so (sem: SEM-docker-compose.yaml-009)
# nothing resolves a home for it. `KeyStore::user` refuses to guess (it errors when neither
# `HOME` nor `USERPROFILE` is set), which is the right refusal and makes this line mandatory
# rather than optional.
HOME: /home/gx
volumes:
# 🔴 The keys are a **second** volume, and the separation is req/56 §3's rather than tidiness:
# a backup of the ledger volume (47 §4's runbook) must not carry the signing key with it.
- gx-keys:/home/gx/.gx/keys
# 47 §1(c)'s named volume: the append-only tile store and everything else req/56 §2 puts under
# `.gx/` (ledger, checkpoints, evidence, receipts, index, drafts, config.toml, VERSION).
# 🔴 One volume for the whole directory rather than one per subdirectory: 47 §4's backup
# runbook is "take a file-level snapshot of the tile store" followed by `gx log consistency`, and a (sem: SEM-docker-compose.yaml-010)
# snapshot that captured the ledger without the checkpoints it is verified against would be a
# backup that cannot be checked.
- gx-state:/project/.gx
# The token, read-only, as a file. Not an environment variable: an env var is visible to every
# process in the namespace and is printed by `docker inspect`.
- ./gx-token:/run/secrets/gx-token:ro
# The binary is static and the image is `scratch`: there is no shell, so a `healthcheck` with a
# `CMD-SHELL` test would fail for the wrong reason. `GET /healthz` is 44 §2.6's unauthenticated
# endpoint and hand 6 made shutdown stage 1 cover it deliberately — a load balancer's health
# check is the first request that must stop succeeding when a server is leaving. Checking it
# needs a client the image does not carry, so the check is left to the operator rather than
# written as a line that would always report unhealthy.
restart: unless-stopped
volumes:
gx-state:
gx-keys: