-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathDockerfile.server
More file actions
242 lines (219 loc) · 10.3 KB
/
Copy pathDockerfile.server
File metadata and controls
242 lines (219 loc) · 10.3 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
# syntax=docker/dockerfile:1.7
# goose ACP server image.
#
# Replaces the old goosed image. Upstream deleted crates/goose-server, so there
# is no goosed binary any more: the server is `goose serve`, which speaks ACP
# (JSON-RPC 2.0) over HTTP and WebSocket at /acp.
#
# Build this through scripts/build-image.sh — it computes the git metadata below
# and picks an immutable tag. Direct builds work too:
#
# docker build -f Dockerfile.server -t goose-acp:local .
# docker compose build
#
# Two mounts are not optional for the ComplianceCow setup:
#
# config.yaml the compliancecow extension must list
# allowed_headers: [Authorization, X-Cow-Security-Context].
# Without it every MCP call is unauthenticated and 401s.
# recipes/ goose resolves cow-<session_type> from GOOSE_RECIPE_PATH or
# ~/.config/goose/recipes. A missing recipe fails SOFT — a warning,
# then the session runs with default extensions instead.
# uv is a runtime dependency, not a build one: goose spawns `uvx` directly for
# inline-Python extensions — `uvx --with mcp [--with dep] python <file>` in
# extension_manager.rs — and stdio extensions configured as `cmd: uvx`.
ARG UV_VERSION=0.12.7
FROM ghcr.io/astral-sh/uv:${UV_VERSION} AS uv
FROM rust:1.96-bookworm AS builder
# python3 is only needed if code-mode is re-enabled (see FEATURES below): that
# feature pulls v8-goose, whose build script downloads a prebuilt librusty_v8
# using Python. Kept here so raising FEATURES does not also require editing apt.
RUN apt-get update && \
apt-get install -y --no-install-recommends \
build-essential \
cmake \
pkg-config \
libssl-dev \
libdbus-1-dev \
libclang-dev \
protobuf-compiler \
libprotobuf-dev \
python3 \
ca-certificates \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /build
COPY . .
ENV CARGO_REGISTRIES_CRATES_IO_PROTOCOL=sparse
ENV CARGO_PROFILE_RELEASE_STRIP=true
# 0 means "let cargo decide", which is one job per core — the right default on a
# native Linux builder, where the daemon uses host RAM directly.
#
# Cap it only when memory is the binding constraint, not cores: this workspace
# needs roughly 2GB per concurrent rustc, and `goose` (lib) alone was SIGKILLed
# under 4GB after 364 crates had already built. Docker Desktop is the usual
# offender because it hands the builder every host core inside a small VM.
#
# BUILD_JOBS = min(nproc, RAM_GB / 2)
#
# Override with --build-arg BUILD_JOBS=N.
ARG BUILD_JOBS=0
# goose-cli's default features include local-inference (llama-cpp, candle,
# tokenizers, symphonia) and code-mode (v8) — everything needed to run a model
# on the same machine. This server calls a remote LLM, so none of it is used,
# and compiling it made `goose` (lib) alone exceed a 4GB builder's memory. The
# trim also cuts the dependency graph from 952 crates to 540.
#
# The fork's own features are not behind any of these gates — §4 header
# forwarding, the custom ACP methods, §6/§9 provider behaviour and §12 subagent
# tenancy are all in the core lib — so trimming here does not touch them. The
# assertion below is what proves that, per build, rather than assuming it.
#
# Re-add features here if you need them, e.g.
# --build-arg FEATURES=rustls-tls,code-mode
ARG FEATURES=rustls-tls
# Almost all of a release build is LLVM optimising code, and this server spends
# its life waiting on a remote LLM and a local SQLite file — it is not CPU-bound,
# so most of that work buys nothing at run time.
#
# --build-arg OPT_LEVEL=1 --build-arg CODEGEN_UNITS=256
#
# cuts a cold build substantially. Defaults are cargo's own release settings, so
# the image is unchanged unless you ask for the faster build.
ARG OPT_LEVEL=3
ARG CODEGEN_UNITS=16
ENV CARGO_PROFILE_RELEASE_OPT_LEVEL=${OPT_LEVEL} \
CARGO_PROFILE_RELEASE_CODEGEN_UNITS=${CODEGEN_UNITS}
# Cache mounts keep the registry and target dir across builds. The binary lives
# inside the cached target dir, which is not part of the image layer, so it must
# be copied out inside the same RUN.
RUN --mount=type=cache,target=/usr/local/cargo/registry \
--mount=type=cache,target=/usr/local/cargo/git \
--mount=type=cache,target=/build/target \
set -eu && \
if [ "${BUILD_JOBS}" != "0" ]; then export CARGO_BUILD_JOBS="${BUILD_JOBS}"; fi && \
cargo build --release --package goose-cli --bin goose \
--no-default-features --features "${FEATURES}" && \
cp /build/target/release/goose /usr/local/bin/goose
# Fail the build if the fork's own ACP methods are missing from the binary.
#
# This exists because an image built from `main` instead of `acp-migration` is
# indistinguishable from a correct one until runtime, where it presents as
# `-32601: Method not found` on session/provider/update — an error CowGooseService
# SWALLOWS. The session then runs with the wrong provider, and §4 tenant headers
# never reach cow-mcp, so MCP calls 401 with nothing obviously wrong upstream.
#
# Only these two are checked, and only these two are worth checking. They are the
# fork's own additions, so their absence is exactly what "built from the wrong
# branch" looks like. The upstream session-admin methods (info, rename, export)
# were in this list and had to come out: their literals do not survive the release
# build's optimisation, so the check failed on a perfectly good binary. Their
# dispatchers carry no cfg or feature gate, so they are compiled in regardless —
# but grep cannot prove that, and a check that cries wolf is worse than none.
#
# grep -a rather than `strings` so this does not depend on binutils.
RUN set -eu; \
missing=""; \
for m in \
"_goose/unstable/session/provider/update" \
"_goose/unstable/session/extension_data/set" ; do \
grep -aqF "$m" /usr/local/bin/goose || missing="${missing} ${m}"; \
done; \
if [ -n "$missing" ]; then \
echo "=============================================================="; \
echo "FORK METHODS MISSING from the built binary:"; \
for m in $missing; do echo " $m"; done; \
echo; \
echo "This binary was almost certainly built from a branch without the"; \
echo "ComplianceCow fork changes. Check out acp-migration and rebuild."; \
echo "=============================================================="; \
exit 1; \
fi; \
echo "fork ACP methods present"
# Runtime stage — minimal Debian
FROM debian:bookworm-slim
RUN apt-get update && \
apt-get install -y --no-install-recommends \
ca-certificates \
libssl3 \
libdbus-1-3 \
libgomp1 \
libxcb1 \
curl \
git \
sqlite3 \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*
COPY --from=builder /usr/local/bin/goose /usr/local/bin/goose
COPY --from=uv /uv /uvx /usr/local/bin/
# uv provides the interpreter, so there is no system python3 in this image.
#
# Installed at build time on purpose. uv would otherwise fetch a managed Python
# on first use, which turns the first inline-Python extension of a container's
# life into a ~30MB download — a slow cold start, and a hard failure wherever
# egress to python-build-standalone is blocked.
#
# a+rX because the interpreter lives outside $HOME and is read by uid 1000.
ARG PYTHON_VERSION=3.12
ENV UV_PYTHON_INSTALL_DIR=/opt/uv/python
RUN uv python install "${PYTHON_VERSION}" && chmod -R a+rX /opt/uv
# First-party MCP servers. Any direct subdirectory of mcp-servers/ with a
# pyproject.toml is installed here, and its console script lands in
# /usr/local/bin — see mcp-servers/README.md.
#
# The loop is a no-op when the directory holds only its README, so this costs
# nothing until a server is actually added.
COPY mcp-servers /opt/goose/mcp-servers
ENV UV_TOOL_DIR=/opt/uv/tools \
UV_TOOL_BIN_DIR=/usr/local/bin
RUN set -eu; \
for d in /opt/goose/mcp-servers/*/; do \
[ -f "${d}pyproject.toml" ] || continue; \
echo "installing MCP server: $(basename "$d")"; \
uv tool install "$d"; \
done; \
chmod -R a+rX /opt/uv
RUN useradd -m -u 1000 -s /bin/bash goose && \
mkdir -p /home/goose/.config/goose /home/goose/recipes \
/home/goose/.cache/uv /home/goose/.local/share/uv /home/goose/.local/bin && \
chown -R goose:goose /home/goose
# Provenance. .dockerignore excludes .git/, so these cannot be computed inside
# the build — build-image.sh passes them in. Recorded as ENV as well as LABEL so
# `kubectl exec <pod> -- env | grep GOOSE_IMAGE` answers "which branch is this?"
# without needing registry access to inspect labels.
ARG GIT_REF=unknown
ARG GIT_SHA=unknown
ARG BUILD_DATE=unknown
ENV GOOSE_IMAGE_REF=${GIT_REF} \
GOOSE_IMAGE_REVISION=${GIT_SHA}
# GOOSE__HOST and GOOSE__PORT are deliberately absent: they belonged to goosed
# and nothing reads them now, so setting them would describe a configuration the
# server does not have. Host and port are flags on the CMD below.
#
# `goose serve` refuses to start unless GOOSE_SERVER__SECRET_KEY is set or
# --dangerously-unauthenticated is passed. Supply the key at run time;
# CowGooseService already sends it as the x-secret-key header.
ENV HOME=/home/goose \
GOOSE_DISABLE_KEYRING=1 \
GOOSE_RECIPE_PATH=/home/goose/recipes
# Only the cache is per-user and writable. Servers from mcp-servers/ are baked
# into a shared, read-only tool dir above, with their executables on the default
# PATH — so `cmd: veza-query-mcp` in config.yaml resolves with no path and no
# resolution step. `uvx --with <dep>` still reaches PyPI on every start, so a
# cluster without egress needs its servers baked in rather than fetched.
ENV UV_CACHE_DIR=/home/goose/.cache/uv
USER goose
WORKDIR /home/goose
EXPOSE 3000
ENTRYPOINT ["/usr/local/bin/goose"]
# goose's own default is 127.0.0.1:3284, which is unreachable from outside the
# container. The port must match what CowGooseService dials — a mismatch there
# presents as a hang, not an error.
CMD ["serve", "--host", "0.0.0.0", "--port", "3000"]
# Static, not derived from `git remote get-url`: the local remotes carry an
# embedded access token, which would be published with the image.
LABEL org.opencontainers.image.title="goose-acp" \
org.opencontainers.image.description="goose ACP server (goose serve)" \
org.opencontainers.image.source="https://github.com/opensecuritycompliance/goose" \
org.opencontainers.image.revision="${GIT_SHA}" \
org.opencontainers.image.version="${GIT_REF}" \
org.opencontainers.image.created="${BUILD_DATE}"