-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathdocker-compose.yml
More file actions
151 lines (146 loc) · 9.45 KB
/
Copy pathdocker-compose.yml
File metadata and controls
151 lines (146 loc) · 9.45 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
# Local dev stack. Spins up the dependencies CodeSpace needs so `dotnet run` and
# `pnpm dev` work without any extra setup.
#
# docker compose up -d # start everything in the background
# docker compose down # stop containers, preserve data
# docker compose down -v # nuke the volume too (fresh DB)
#
# Credentials below intentionally match backend/src/CodeSpace.Api/appsettings.json —
# the API connects with codespace/codespace/codespace out of the box. Rotate via env
# vars in any non-local environment.
services:
postgres:
image: postgres:18-alpine
container_name: codespace-postgres
restart: unless-stopped
environment:
POSTGRES_USER: codespace
POSTGRES_PASSWORD: codespace
POSTGRES_DB: codespace
# Also create a postgres superuser so the integration test fixture
# (TestPostgres:AdminConnectionString) can spin up disposable per-test DBs.
POSTGRES_HOST_AUTH_METHOD: scram-sha-256
# PG18+ uses major-version-specific data layout; pin PGDATA to a subdirectory of the
# mount so the volume-mount semantics we want (single named volume → all data) stay
# intact while satisfying PG18's check that data isn't at /var/lib/postgresql/data
# directly. See docker-library/postgres#37.
PGDATA: /var/lib/postgresql/data/pgdata
ports:
- "5432:5432"
volumes:
- codespace-pgdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U codespace -d codespace"]
interval: 5s
timeout: 3s
retries: 10
# The CodeSpace app, split into TWO deployments off TWO images that mirror the production topology — the public
# HTTP frontend and the local sandbox runner are different security/scaling concerns, so they are different
# images (backend/Dockerfile.api + backend/Dockerfile.worker). Both run the SAME CodeSpace.Api.dll; the ROLE is
# selected by the HangfireHosting setting (Api | Worker), read through the normal configuration pipeline. Both are behind the "app" profile so the default local-dev
# flow stays "compose up Postgres + `dotnet run`"; run the full containerised stack with
# `docker compose --profile app up`. In k8s these become two Deployments that scale (replica) independently.
# PUBLIC API — serves HTTP + enqueues, processes NO jobs (HangfireHosting=Api → no Hangfire server). It executes no agent and hosts no MCP endpoint, so it carries none of the agent-execution /
# isolation deps (lean, internet-facing → small attack surface). Built from the minimal Dockerfile.api.
api:
profiles: ["app"]
build:
context: ./backend
dockerfile: Dockerfile.api
container_name: codespace-api
restart: unless-stopped
depends_on:
postgres:
condition: service_healthy
ports:
- "5099:8080"
environment:
ASPNETCORE_ENVIRONMENT: Production
ASPNETCORE_URLS: http://+:8080
CodeSpaceStore__ConnectionString: "Host=postgres;Port=5432;Database=codespace;Username=codespace;Password=codespace"
# Blank turns Seq off, which is what a container gets by default: appsettings points at
# localhost:5341, and inside a container that is the container itself. Point it at a reachable
# Seq (host.docker.internal, or a service you add) to search these logs afterwards.
Serilog__Seq__ServerUrl: "${SEQ_SERVER_URL:-}"
# This pod NEVER processes jobs — it serves HTTP + enqueues only. Agent execution lives in the worker.
HangfireHosting: "Api"
# ASPNETCORE_ENVIRONMENT=Production above means the variable-encryption master key is REQUIRED (Development is
# the only environment with a fallback), so without these two the pod throws at container build and crash-loops.
# The values are LOCAL-ONLY and labelled as such; override both in anything an operator would call a deployment.
CODESPACE_VARIABLE_MASTER_KEY: "${CODESPACE_VARIABLE_MASTER_KEY:-bG9jYWwtZGV2LW9ubHkta2V5LW5vdC1mb3ItcHJvZCE=}"
# Stated explicitly rather than inheriting appsettings.json's committed dev key — a Production-labelled pod
# silently signing tokens with a source-controlled key is exactly the footgun this line makes visible.
Authentication__Jwt__SymmetricKey: "${CODESPACE_JWT_SYMMETRIC_KEY:-local-dev-only-jwt-key-minimum-32-chars-long}"
# Both app roles mount the same named artifact volume below, so local-rwx is a genuinely shared namespace.
Artifacts__LocalRwxShared: "true"
volumes:
# Artifact metadata lives in Postgres, but offloaded bytes must be visible to both roles. An image-layer path
# has the same spelling in each container while still being a different filesystem, and disappears on recreate.
- codespace-artifacts:/var/lib/codespace/artifacts
# WORKER = THE LOCAL SANDBOX RUNNER — the Hangfire PROCESSING pod (HangfireHosting=Worker, which is also the
# default when the key is absent). Agent runs + the per-run MCP endpoint execute HERE; the harness CLI is spawned as a confined child
# by LocalProcessRunner. Built from Dockerfile.worker, which carries git + bubblewrap + util-linux (prlimit) +
# the codespace-mcp proxy. The MCP endpoint + tool governance are turned ON here; code defaults stay fail-closed
# default-OFF (a deployment that does not set these is byte-identical to today). Every flag is ${VAR:-default}.
#
# SAFETY: read-only tools are served UNCONDITIONALLY (McpRequestHandler short-circuits IsReadOnly BEFORE the
# governance ledger); side-effecting tools route through the exactly-once ToolCallLedger + AgentToolGate matrix
# (fail-closed). CONFINEMENT: bubblewrap is in the image, but Docker's default seccomp and AppArmor profiles deny it
# the user namespaces and mounts it needs, and Docker's masked /proc blocks the fresh /proc it mounts, so by default
# local dev runs agents unconfined-in-container and Sandbox:RequireConfinement is NOT armed. The commented
# security_opt block below grants all three without --privileged or any capability (Dockerfile.worker has the same
# posture for Kubernetes).
worker:
profiles: ["app"]
build:
context: ./backend
dockerfile: Dockerfile.worker
container_name: codespace-worker
restart: unless-stopped
# Confine agent runs as the image's uid 1654 with no capabilities: uncomment on a host that allows unprivileged
# user namespaces (Compose v2.15+ for systempaths), then arm Sandbox__RequireConfinement below. The worker's boot
# line then reads "Sandbox posture: bubblewrap confines True", but that probe mounts no /proc: without the
# systempaths line it still reads True while every launch fails with bubblewrap's "Can't mount proc on
# /newroot/proc" in the run's stderr, so check that the first run's agent actually starts.
# security_opt:
# - seccomp=./backend/deploy/seccomp/codespace-worker.json
# - apparmor=unconfined
# - systempaths=unconfined
depends_on:
postgres:
condition: service_healthy
ports:
- "5100:8080" # the Hangfire dashboard (/hangfire) + internal HTTP; the PUBLIC surface is the api service
environment:
ASPNETCORE_ENVIRONMENT: Production
ASPNETCORE_URLS: http://+:8080
CodeSpaceStore__ConnectionString: "Host=postgres;Port=5432;Database=codespace;Username=codespace;Password=codespace"
# Blank turns Seq off, which is what a container gets by default: appsettings points at
# localhost:5341, and inside a container that is the container itself. Point it at a reachable
# Seq (host.docker.internal, or a service you add) to search these logs afterwards.
Serilog__Seq__ServerUrl: "${SEQ_SERVER_URL:-}"
# This pod IS the worker — it executes agent runs + the per-run MCP endpoint. Stated explicitly rather than
# relying on the default, so the topology is readable from the compose file alone.
HangfireHosting: "Worker"
# Both pods share ONE key-ring and ONE token signer, so these MUST match the api service's values above.
CODESPACE_VARIABLE_MASTER_KEY: "${CODESPACE_VARIABLE_MASTER_KEY:-bG9jYWwtZGV2LW9ubHkta2V5LW5vdC1mb3ItcHJvZCE=}"
Authentication__Jwt__SymmetricKey: "${CODESPACE_JWT_SYMMETRIC_KEY:-local-dev-only-jwt-key-minimum-32-chars-long}"
Artifacts__LocalRwxShared: "true"
# The full tool fabric and its governance used to be turned on here. Both are committed constants now
# (AgentRunExecutor.FullToolCatalogByDefault, McpRequestHandler.GovernanceEnabled) at exactly the values this
# file set, so every deployment gets the posture this one had rather than whatever its env happened to say.
# The codespace-mcp proxy is published next to the API by the csproj target, so the default path resolves;
# set this only to point at an air-gapped mirror or a custom location.
CODESPACE_MCP_PROXY_PATH: "${CODESPACE_MCP_PROXY_PATH:-}"
# Arm the fail-closed isolation guard wherever confinement is granted (the security_opt block above, or a prod pod
# with the same posture), so a lost grant refuses runs instead of running them unconfined; off for local dev.
# Sandbox__RequireConfinement: "true"
volumes:
- codespace-artifacts:/var/lib/codespace/artifacts
# The spool is the recoverable source until stdout/stderr segments reach artifact CAS. Keep it across worker
# replacement and share it between same-host worker replicas; production multi-host deployments require RWX.
- codespace-spool:/var/lib/codespace/spool
volumes:
codespace-pgdata:
codespace-artifacts:
codespace-spool: